diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de3791de8d..8ca75f05ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,8 +35,8 @@ jobs: #--------------------------------------------------- # RUN - - name: Build Debug APK - run: ./gradlew assembleDebug + - name: Build demo APK + run: ./gradlew assembleDemo - name: Set commit SHA as an output so it can be included in the APK filename id: vars @@ -46,5 +46,5 @@ jobs: uses: actions/upload-artifact@v2 with: name: "Ivy-Wallet-${{ steps.vars.outputs.sha_short }}.apk" - path: app/build/outputs/apk/debug/app-debug.apk + path: app/build/outputs/apk/demo/app-demo.apk #------------------------------------------------------------------ diff --git a/README.md b/README.md index 2bb82df192..5352223528 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Latest Release](https://img.shields.io/github/v/release/Ivy-Apps/ivy-wallet)](https://github.com/Ivy-Apps/ivy-wallet/releases) -[![Build](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/build.yml/badge.svg)](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/internal_release.yml) +[![Build](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/build.yml/badge.svg)](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/build.yml) [![Lint](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/lint.yml/badge.svg)](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/lint.yml) [![Unit Test](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/unit_test.yml/badge.svg)](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/unit_test.yml) [![Integration Test](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/integration_test.yml/badge.svg)](https://github.com/Ivy-Apps/ivy-wallet/actions/workflows/integration_test.yml) @@ -21,7 +21,7 @@ Imagine Ivy Wallet as a manual expense tracker that will replace the good old sp Track your expenses, fast and on-the-go! ⚡ Discover powerful insights about your spending. -**Do you know? Ask yoursef.** +**Do you know? Ask yourself.** 1) How much money do I have right now in all accounts combined? @@ -107,7 +107,7 @@ in **[docs/resources 📚](docs/resources/)**. 1. Clone the repository 2. Open with Android Studio 3. Everything should sync and build automatically -- _If any build problems occurr, please [open a new issue](https://github.com/Ivy-Apps/ivy-wallet/issues/new?assignees=&labels=dev&template=dev-contributor-request.yml) including the logs._ +- _If any build problems occur, please [open a new issue](https://github.com/Ivy-Apps/ivy-wallet/issues/new?assignees=&labels=dev&template=dev-contributor-request.yml) including the logs._ ## Ideology :earth_africa: We believe that people _(not corporations)_ can create innovative, open-source, diff --git a/accounts/build.gradle.kts b/accounts/build.gradle.kts index 04fe0b73d7..f5ceb1f26e 100644 --- a/accounts/build.gradle.kts +++ b/accounts/build.gradle.kts @@ -1,5 +1,5 @@ -import com.ivy.buildsrc.EventBus import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing apply() @@ -15,12 +15,6 @@ dependencies { implementation(project(":core:ui")) implementation(project(":core:domain")) implementation(project(":navigation")) - - // TODO: Remove these - implementation(project(":temp-domain")) - implementation(project(":app-base")) - implementation(project(":temp-persistence")) - implementation(project(":ui-components-old")) - - EventBus() + implementation(project(":main:base")) + Testing() } \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/AccountEvent.kt b/accounts/src/main/java/com/ivy/accounts/AccountEvent.kt deleted file mode 100644 index 8e7757225c..0000000000 --- a/accounts/src/main/java/com/ivy/accounts/AccountEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.accounts - -import com.ivy.base.AccountData -import com.ivy.data.AccountOld - -sealed interface AccountEvent { - data class OnReorder(val reorderedList: List) : AccountEvent - data class OnEditAccount(val editedAccount: AccountOld, val newBalance: Double) : AccountEvent - data class OnReorderModalVisible(val reorderVisible: Boolean) : AccountEvent - - data class BottomBarAction(val action: AccBottomBarAction) : AccountEvent -} \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/AccountState.kt b/accounts/src/main/java/com/ivy/accounts/AccountState.kt deleted file mode 100644 index 346af1f6c2..0000000000 --- a/accounts/src/main/java/com/ivy/accounts/AccountState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.accounts - -data class AccountState( - val dummy: String, -) \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/AccountTab.kt b/accounts/src/main/java/com/ivy/accounts/AccountTab.kt index 26039e6d73..258a639728 100644 --- a/accounts/src/main/java/com/ivy/accounts/AccountTab.kt +++ b/accounts/src/main/java/com/ivy/accounts/AccountTab.kt @@ -1,372 +1,226 @@ -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.runtime.Composable -import com.ivy.design.util.ScreenPlaceholder +package com.ivy.accounts -//package com.ivy.accounts -// -//import androidx.compose.foundation.background -//import androidx.compose.foundation.border -//import androidx.compose.foundation.clickable -//import androidx.compose.foundation.layout.* -//import androidx.compose.foundation.lazy.LazyColumn -//import androidx.compose.foundation.lazy.items -//import androidx.compose.material.Text -//import androidx.compose.runtime.Composable -//import androidx.compose.runtime.collectAsState -//import androidx.compose.runtime.getValue -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.draw.clip -//import androidx.compose.ui.graphics.Color -//import androidx.compose.ui.graphics.toArgb -//import androidx.compose.ui.platform.testTag -//import androidx.compose.ui.res.stringResource -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.compose.ui.unit.sp -//import androidx.hilt.navigation.compose.hiltViewModel -//import com.ivy.base.AccountData -//import com.ivy.base.UiText -//import com.ivy.data.AccountOld -//import com.ivy.design.l0_system.UI -//import com.ivy.design.l0_system.style -//import com.ivy.design.util.IvyPreview -// -// -//import com.ivy.wallet.ui.theme.* -//import com.ivy.wallet.ui.theme.components.* -//import com.ivy.wallet.utils.clickableNoIndication -//import com.ivy.wallet.utils.horizontalSwipeListener -// +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.accounts.components.accountsList +import com.ivy.accounts.data.AccountListItemUi +import com.ivy.accounts.modal.CreateModal +import com.ivy.accounts.modal.NetWorthInfoModal +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.account.create.CreateAccountModal +import com.ivy.core.ui.account.edit.EditAccountModal +import com.ivy.core.ui.account.folder.create.CreateAccFolderModal +import com.ivy.core.ui.account.folder.edit.EditAccFolderModal +import com.ivy.core.ui.account.reorder.ReorderAccountsModal +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.account.dummyFolderUi +import com.ivy.core.ui.value.AmountCurrency +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.Blue +import com.ivy.design.l0_system.color.Red +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.ReorderButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe @Composable fun BoxScope.AccountTab() { - ScreenPlaceholder(text = "Accounts") + val viewModel: AccountTabViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value + ?: previewState() + + UI(state = state, onEvent = { viewModel?.onEvent(it) }) +} + +@Composable +private fun BoxScope.UI( + state: AccountTabState, + onEvent: (AccountTabEvent) -> Unit, +) { + val editAccountModal = rememberIvyModal() + var editAccountId by remember { mutableStateOf(null) } + val editFolderModal = rememberIvyModal() + var editFolderId by remember { mutableStateOf(null) } + + val reorderModal = rememberIvyModal() + val createAccountModal = rememberIvyModal() + val netWorthInfoModal = rememberIvyModal() + + BackHandler(enabled = true) { + onEvent(AccountTabEvent.NavigateToHome) + } + + val lazyListState = rememberLazyListState() + val firstVisibleItemIndex by remember { + derivedStateOf { lazyListState.firstVisibleItemIndex } + } + LaunchedEffect(firstVisibleItemIndex) { + if (firstVisibleItemIndex > 0) { + onEvent(AccountTabEvent.HideBottomBar) + } else { + onEvent(AccountTabEvent.ShowBottomBar) + } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding(), + state = lazyListState, + ) { + item(key = "header") { + SpacerVer(height = 16.dp) + Header( + totalBalance = state.totalBalance, + onNetWorthClick = { + netWorthInfoModal.show() + }, + onReorder = { + reorderModal.show() + } + ) + SpacerVer(height = 4.dp) + } + accountsList( + items = state.items, + noAccounts = state.noAccounts, + onAccountClick = { + editAccountId = it.id + editAccountModal.show() + }, + onFolderClick = { + editFolderId = it.id + editFolderModal.show() + }, + onCreateAccount = { + createAccountModal.show() + } + ) + item { + SpacerVer(height = 300.dp) // last item spacer + } + } + + val createFolderModal = rememberIvyModal() + CreateModal( + modal = state.createModal, + onCreateAccount = { createAccountModal.show() }, + onCreateFolder = { createFolderModal.show() } + ) + CreateAccountModal(modal = createAccountModal) + CreateAccFolderModal(modal = createFolderModal) + + editAccountId?.let { + EditAccountModal(modal = editAccountModal, accountId = it) + } + editFolderId?.let { + EditAccFolderModal(modal = editFolderModal, folderId = it) + } + + NetWorthInfoModal( + modal = netWorthInfoModal, + totalBalance = state.totalBalance, + availableBalance = state.availableBalance, + excludedBalance = state.excludedBalance, + ) + ReorderAccountsModal(modal = reorderModal) +} + +@Composable +private fun Header( + totalBalance: ValueUi, + onNetWorthClick: () -> Unit, + onReorder: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .clickable(onClick = onNetWorthClick) + ) { + B1(text = "Net-worth") + Row( + verticalAlignment = Alignment.CenterVertically + ) { + AmountCurrency(totalBalance, color = UI.colors.primary) + } + } + SpacerHor(width = 4.dp) + ReorderButton(onClick = onReorder) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + AccountTab() + } } -//@Composable -//fun BoxWithConstraintsScope.AccountsTab() { -// val viewModel: AccountsViewModel = hiltViewModel() -// val state by viewModel.state().collectAsState() -// -// UI( -// state = state, -// onEventHandler = viewModel::onEvent -// ) -//} -// -//@Composable -//private fun BoxWithConstraintsScope.UI( -// state: AccountState = AccountState(), -// onEventHandler: (AccountsEvent) -> Unit = {} -//) { -// -// -// LazyColumn( -// modifier = Modifier -// .fillMaxSize() -// .statusBarsPadding() -// .navigationBarsPadding() -// .horizontalSwipeListener( -// sensitivity = 200, -// onSwipeLeft = { -//// ivyContext.selectMainTab(com.ivy.base.MainTab.HOME) -// }, -// onSwipeRight = { -//// ivyContext.selectMainTab(com.ivy.base.MainTab.HOME) -// } -// ), -// ) { -// -// item { -// Spacer(Modifier.height(32.dp)) -// -// Row( -// verticalAlignment = Alignment.CenterVertically -// ) { -// Spacer(Modifier.width(24.dp)) -// -// Column { -// Text( -// text = stringResource(R.string.accounts), -// style = UI.typo.b1.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(4.dp)) -// -// Text( -// text = state.totalBalanceWithExcludedText.asString(), -// style = UI.typoSecond.b2.style( -// color = Gray, -// fontWeight = FontWeight.Bold -// ) -// ) -// } -// -// Spacer(Modifier.weight(1f)) -// -// ReorderButton { -// onEventHandler.invoke(AccountsEvent.OnReorderModalVisible(reorderVisible = true)) -// } -// -// Spacer(Modifier.width(24.dp)) -// } -// -// Spacer(Modifier.height(16.dp)) -// } -// -// items(state.accountsData) { -// AccountCard( -// baseCurrency = state.baseCurrency, -// accountData = it, -// onBalanceClick = { -//// nav.navigateTo( -//// ItemStatistic( -//// accountId = it.account.id, -//// categoryId = null -//// ) -//// ) -// }, -// onLongClick = { -// onEventHandler.invoke(AccountsEvent.OnReorderModalVisible(reorderVisible = true)) -// } -// ) { -//// nav.navigateTo( -//// ItemStatistic( -//// accountId = it.account.id, -//// categoryId = null -//// ) -//// ) -// } -// } -// -// item { -// Spacer(Modifier.height(150.dp)) //scroll hack -// } -// } -// -// ReorderModalSingleType( -// visible = state.reorderVisible, -// initialItems = state.accountsData, -// dismiss = { -// onEventHandler.invoke(AccountsEvent.OnReorderModalVisible(reorderVisible = false)) -// }, -// onReordered = { -// onEventHandler.invoke(AccountsEvent.OnReorder(reorderedList = it)) -// } -// ) { _, item -> -// Text( -// modifier = Modifier -// .fillMaxWidth() -// .padding(end = 24.dp) -// .padding(vertical = 8.dp), -// text = item.account.name, -// style = UI.typo.b1.style( -// color = item.account.color.toComposeColor(), -// fontWeight = FontWeight.Bold -// ) -// ) -// } -//} -// -//@Composable -//private fun AccountCard( -// baseCurrency: String, -// accountData: AccountData, -// onBalanceClick: () -> Unit, -// onLongClick: () -> Unit, -// onClick: () -> Unit -//) { -// val account = accountData.account -// val contrastColor = findContrastTextColor(account.color.toComposeColor()) -// -// Spacer(Modifier.height(16.dp)) -// -// Column( -// modifier = Modifier -// .padding(horizontal = 16.dp) -// .fillMaxWidth() -// .clip(UI.shapes.squared) -// .border(2.dp, UI.colors.medium, UI.shapes.squared) -// .clickable( -// onClick = onClick -// ) -// ) { -// val currency = account.currency ?: baseCurrency -// -// AccountHeader( -// accountData = accountData, -// currency = currency, -// baseCurrency = baseCurrency, -// contrastColor = contrastColor, -// -// onBalanceClick = onBalanceClick -// ) -// -// Spacer(Modifier.height(12.dp)) -// -// IncomeExpensesRow( -// currency = currency, -// incomeLabel = stringResource(R.string.month_income), -// income = accountData.monthlyIncome, -// expensesLabel = stringResource(R.string.month_expenses), -// expenses = accountData.monthlyExpenses -// ) -// -// Spacer(Modifier.height(12.dp)) -// } -//} -// -//@Composable -//private fun AccountHeader( -// accountData: AccountData, -// currency: String, -// baseCurrency: String, -// contrastColor: Color, -// -// onBalanceClick: () -> Unit -//) { -// val account = accountData.account -// -// Column( -// modifier = Modifier -// .fillMaxWidth() -// .background(account.color.toComposeColor(), UI.shapes.squaredTop) -// ) { -// Spacer(Modifier.height(16.dp)) -// -// Row( -// verticalAlignment = Alignment.CenterVertically -// ) { -// Spacer(Modifier.width(20.dp)) -// -// ItemIconSDefaultIcon( -// iconName = account.icon, -// defaultIcon = R.drawable.ic_custom_account_s, -// tint = contrastColor -// ) -// -// Spacer(Modifier.width(8.dp)) -// -// Text( -// text = account.name, -// style = UI.typo.b1.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// if (!account.includeInBalance) { -// Spacer(Modifier.width(8.dp)) -// -// Text( -// modifier = Modifier -// .align(Alignment.Bottom) -// .padding(bottom = 4.dp), -// text = stringResource(R.string.excluded), -// style = UI.typo.c.style( -// color = account.color.toComposeColor().dynamicContrast() -// ) -// ) -// } -// } -// -// Spacer(Modifier.height(4.dp)) -// -// BalanceRow( -// modifier = Modifier -// .align(Alignment.CenterHorizontally) -// .clickableNoIndication { -// onBalanceClick() -// }, -// decimalPaddingTop = 7.dp, -// spacerDecimal = 6.dp, -// textColor = contrastColor, -// currency = currency, -// balance = accountData.balance, -// -// integerFontSize = 30.sp, -// decimalFontSize = 18.sp, -// currencyFontSize = 30.sp, -// -// currencyUpfront = false -// ) -// -// if (currency != baseCurrency && accountData.balanceBaseCurrency != null) { -// BalanceRowMini( -// modifier = Modifier -// .align(Alignment.CenterHorizontally) -// .clickableNoIndication { -// onBalanceClick() -// } -// .testTag("baseCurrencyEquivalent"), -// textColor = account.color.toComposeColor().dynamicContrast(), -// currency = baseCurrency, -// balance = accountData.balanceBaseCurrency!!, -// currencyUpfront = false -// ) -// } -// -// Spacer(Modifier.height(16.dp)) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewAccountsTab() { -// IvyPreview { -// val state = AccountState( -// baseCurrency = "BGN", -// accountsData = listOf( -// AccountData( -// account = AccountOld("Phyre", color = Green.toArgb()), -// balance = 2125.0, -// balanceBaseCurrency = null, -// monthlyExpenses = 920.0, -// monthlyIncome = 3045.0 -// ), -// AccountData( -// account = AccountOld("DSK", color = GreenLight.toArgb()), -// balance = 12125.21, -// balanceBaseCurrency = null, -// monthlyExpenses = 1350.50, -// monthlyIncome = 8000.48 -// ), -// AccountData( -// account = AccountOld( -// "Revolut", -// color = IvyDark.toArgb(), -// currency = "USD", -// icon = "revolut", -// includeInBalance = false -// ), -// balance = 1200.0, -// balanceBaseCurrency = 1979.64, -// monthlyExpenses = 750.0, -// monthlyIncome = 1000.30 -// ), -// AccountData( -// account = AccountOld( -// "Cash", -// color = GreenDark.toArgb(), -// icon = "cash" -// ), -// balance = 820.0, -// balanceBaseCurrency = null, -// monthlyExpenses = 340.0, -// monthlyIncome = 400.0 -// ), -// ), -// totalBalanceWithExcluded = 25.54, -// totalBalanceWithExcludedText = UiText.StringResource( -// R.string.total, "BGN", "25.54" -// ) -// ) -// -// UI(state = state) -// } -//} +private fun previewState() = AccountTabState( + totalBalance = dummyValueUi("203k"), + availableBalance = dummyValueUi("136,3k"), + excludedBalance = dummyValueUi("64,3k"), + noAccounts = false, + items = listOf( + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Cash"), + balance = dummyValueUi("240.75"), + balanceBaseCurrency = null, + ), + AccountListItemUi.FolderWithAccounts( + folder = dummyFolderUi("Business"), + balance = dummyValueUi("5,320.50"), + accItems = listOf( + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Account 1"), + balance = dummyValueUi("1,000.00", "BGN"), + balanceBaseCurrency = dummyValueUi("500") + ), + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Account 2", color = Blue, excluded = true), + balance = dummyValueUi("0.00"), + balanceBaseCurrency = null + ), + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Account 3", color = Red), + balance = dummyValueUi("4,320.50"), + balanceBaseCurrency = null + ), + ), + accountsCount = 3, + ), + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Revolut", color = Blue), + balance = dummyValueUi("1,032.54"), + balanceBaseCurrency = null + ), + AccountListItemUi.Archived( + accHolders = listOf(), + accountsCount = 0, + ) + ), + createModal = IvyModal() +) +// endregion diff --git a/accounts/src/main/java/com/ivy/accounts/AccountTabEvent.kt b/accounts/src/main/java/com/ivy/accounts/AccountTabEvent.kt new file mode 100644 index 0000000000..7515a02d91 --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/AccountTabEvent.kt @@ -0,0 +1,11 @@ +package com.ivy.accounts + +import com.ivy.main.base.MainBottomBarAction + +sealed interface AccountTabEvent { + object NavigateToHome : AccountTabEvent + + data class BottomBarAction(val action: MainBottomBarAction) : AccountTabEvent + object ShowBottomBar : AccountTabEvent + object HideBottomBar : AccountTabEvent +} \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/AccountTabState.kt b/accounts/src/main/java/com/ivy/accounts/AccountTabState.kt new file mode 100644 index 0000000000..c5872af41a --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/AccountTabState.kt @@ -0,0 +1,16 @@ +package com.ivy.accounts + +import androidx.compose.runtime.Immutable +import com.ivy.accounts.data.AccountListItemUi +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.design.l2_components.modal.IvyModal + +@Immutable +data class AccountTabState( + val totalBalance: ValueUi, + val availableBalance: ValueUi, + val excludedBalance: ValueUi, + val noAccounts: Boolean, + val items: List, + val createModal: IvyModal, +) \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/AccountTabViewModel.kt b/accounts/src/main/java/com/ivy/accounts/AccountTabViewModel.kt new file mode 100644 index 0000000000..53c707855c --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/AccountTabViewModel.kt @@ -0,0 +1,208 @@ +package com.ivy.accounts + +import com.ivy.accounts.data.AccountListItemUi +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.folder.AccountFoldersFlow +import com.ivy.core.domain.action.calculate.account.AccBalanceFlow +import com.ivy.core.domain.action.calculate.wallet.TotalBalanceFlow +import com.ivy.core.domain.action.data.AccountListItem +import com.ivy.core.domain.action.exchange.ExchangeFlow +import com.ivy.core.domain.action.exchange.SumValuesInCurrencyFlow +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.format +import com.ivy.core.domain.pure.util.combineList +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.core.ui.action.mapping.account.MapFolderUiAct +import com.ivy.data.Value +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.main.base.MainBottomBarVisibility +import com.ivy.navigation.Navigator +import com.ivy.navigation.destinations.Destination +import com.ivy.navigation.destinations.main.Main +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@HiltViewModel +class AccountTabViewModel @Inject constructor( + private val accountFoldersFlow: AccountFoldersFlow, + private val mapAccountUiAct: MapAccountUiAct, + private val mapFolderUiAct: MapFolderUiAct, + private val accBalanceFlow: AccBalanceFlow, + private val sumValuesInCurrencyFlow: SumValuesInCurrencyFlow, + private val exchangeFlow: ExchangeFlow, + private val totalBalanceFlow: TotalBalanceFlow, + private val navigator: Navigator, + private val mainBottomBarVisibility: MainBottomBarVisibility, +) : SimpleFlowViewModel() { + override val initialUi: AccountTabState = AccountTabState( + totalBalance = ValueUi("", ""), + availableBalance = ValueUi("", ""), + excludedBalance = ValueUi("", ""), + items = emptyList(), + noAccounts = false, + createModal = IvyModal() + ) + + override val uiFlow: Flow = combine( + accListItemsUiFlow(), totalBalanceFlow(), availableBalanceFlow() + ) { items, totalBalance, availableBalance -> + val excludedBalance = Value( + amount = totalBalance.amount - availableBalance.amount, + currency = totalBalance.currency + ) + AccountTabState( + totalBalance = format(totalBalance, shortenFiat = true), + availableBalance = format(availableBalance, shortenFiat = true), + excludedBalance = format(excludedBalance, shortenFiat = true), + noAccounts = items.none { + // no items (accounts) that match the predicate + when (it) { + is AccountListItemUi.AccountWithBalance -> true + is AccountListItemUi.Archived -> it.accountsCount > 0 + is AccountListItemUi.FolderWithAccounts -> it.accountsCount > 0 + } + }, + items = items, + createModal = initialUi.createModal + ) + } + + private fun totalBalanceFlow(): Flow = totalBalanceFlow( + TotalBalanceFlow.Input(withExcludedAccs = true) + ) + + private fun availableBalanceFlow(): Flow = totalBalanceFlow( + TotalBalanceFlow.Input(withExcludedAccs = false) + ) + + // TODO: Re-work this, it's just ugly! + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) + private fun accListItemsUiFlow(): Flow> = + accountFoldersFlow(Unit).map { items -> + items + .filter { + when (it) { + is AccountListItem.AccountHolder -> true + // allow empty folders + is AccountListItem.FolderWithAccounts -> true + is AccountListItem.Archived -> it.accounts.isNotEmpty() + } + } + .map { item -> + when (item) { + is AccountListItem.AccountHolder -> + item to listOf(accBalanceFlow(AccBalanceFlow.Input(item.account))) + is AccountListItem.FolderWithAccounts -> item to item.accounts + .map { accBalanceFlow(AccBalanceFlow.Input(it)) } + is AccountListItem.Archived -> item to item.accounts + .map { accBalanceFlow(AccBalanceFlow.Input(it)) } + } + }.map { (item, balanceFlows) -> + // Handle empty folders with no accounts inside + if (balanceFlows.isEmpty()) + flowOf(item to listOf()) else combine(balanceFlows) { balances -> + item to balances.toList() + } + } + }.flatMapLatest(transform = ::combineList) + .map(::toAccListItemsUi) + .flatMapLatest(transform = ::combineList) + + private suspend fun toAccListItemsUi( + itemBalances: List>> + ): List> = itemBalances.map { (item, balances) -> + when (item) { + is AccountListItem.AccountHolder -> { + val accBalance = balances.first() + exchangeFlow(ExchangeFlow.Input(accBalance)).map { balanceBaseCurrency -> + AccountListItemUi.AccountWithBalance( + account = mapAccountUiAct(item.account), + balance = format(accBalance, shortenFiat = false), + balanceBaseCurrency = balanceBaseCurrency( + baseCurrency = balanceBaseCurrency, + currency = accBalance, + ) + ) + } + } + is AccountListItem.FolderWithAccounts -> combine( + sumValuesInCurrencyFlow(SumValuesInCurrencyFlow.Input(balances)), + combineList(balances.map { exchangeFlow(ExchangeFlow.Input(it)) }) + ) { folderBalance, balancesBaseCurrency -> + AccountListItemUi.FolderWithAccounts( + folder = mapFolderUiAct(item.folder), + accItems = item.accounts.mapIndexed { index, acc -> + AccountListItemUi.AccountWithBalance( + account = mapAccountUiAct(acc), + balance = format(balances[index], shortenFiat = false), + balanceBaseCurrency = balanceBaseCurrency( + baseCurrency = balancesBaseCurrency[index], + currency = balances[index] + ) + ) + }, + accountsCount = item.accounts.size, + balance = format(folderBalance, shortenFiat = true) + ) + } + is AccountListItem.Archived -> combineList( + balances.map { exchangeFlow(ExchangeFlow.Input(it)) } + ).map { balancesBaseCurrency -> + AccountListItemUi.Archived( + accHolders = item.accounts.mapIndexed { index, acc -> + AccountListItemUi.AccountWithBalance( + account = mapAccountUiAct(acc), + balance = format(balances[index], shortenFiat = false), + balanceBaseCurrency = balanceBaseCurrency( + baseCurrency = balancesBaseCurrency[index], + currency = balances[index] + ) + ) + }, + accountsCount = item.accounts.size, + ) + } + } + } + + private fun balanceBaseCurrency( + baseCurrency: Value, + currency: Value + ): ValueUi? = baseCurrency.takeIf { + it.currency != currency.currency && it.amount != 0.0 + }?.let { format(it, shortenFiat = true) } + + + // region Event Handling + override suspend fun handleEvent(event: AccountTabEvent) = when (event) { + is AccountTabEvent.BottomBarAction -> handleBottomBarAction(event) + AccountTabEvent.NavigateToHome -> handleNavigateToHome() + AccountTabEvent.HideBottomBar -> handleHideBottomBar() + AccountTabEvent.ShowBottomBar -> handleShowBottomBar() + } + + private fun handleBottomBarAction(event: AccountTabEvent.BottomBarAction) { + // TODO: Implement special handling for the gesture (swipe up, left, etc) + uiState.value.createModal.show() + } + + private fun handleNavigateToHome() { + navigator.navigate(Destination.main.destination(Main.Tab.Home)) { + popUpTo(Destination.main.route) { + inclusive = true + } + } + } + + private fun handleShowBottomBar() { + mainBottomBarVisibility.visible.value = true + } + + private fun handleHideBottomBar() { + mainBottomBarVisibility.visible.value = false + } + // endregion +} \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/AccountViewModel.kt b/accounts/src/main/java/com/ivy/accounts/AccountViewModel.kt deleted file mode 100644 index 6ce3bc72ad..0000000000 --- a/accounts/src/main/java/com/ivy/accounts/AccountViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.ivy.accounts - -import com.ivy.core.domain.FlowViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import javax.inject.Inject - -@HiltViewModel -class AccountViewModel @Inject constructor( - -) : FlowViewModel() { - override fun initialState() = AccountState(dummy = "dummy") - - override fun initialUiState(): AccountState = initialState() - - override fun stateFlow(): Flow = flow { - - } - - override suspend fun mapToUiState(state: AccountState): AccountState = state - - override suspend fun handleEvent(event: AccountEvent) { - TODO("Not yet implemented") - } - -} \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/components/AccountCard.kt b/accounts/src/main/java/com/ivy/accounts/components/AccountCard.kt new file mode 100644 index 0000000000..14b23cb0a9 --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/components/AccountCard.kt @@ -0,0 +1,145 @@ +package com.ivy.accounts.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.accounts.R +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.core.ui.value.AmountCurrency +import com.ivy.core.ui.value.AmountCurrencySmall +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l0_system.color.rememberDynamicContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.Caption +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.util.ComponentPreview + +@Composable +fun AccountCard( + account: AccountUi, + balance: ValueUi, + balanceBaseCurrency: ValueUi?, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + val dynamicContrast = rememberDynamicContrast(account.color) + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(UI.shapes.rounded) + .background(account.color, UI.shapes.rounded) + .border(1.dp, dynamicContrast, UI.shapes.rounded) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp) + .padding(top = 4.dp, bottom = 12.dp), + ) { + val contrastColor = rememberContrast(account.color) + Header(account = account, color = contrastColor, dynamicContrast = dynamicContrast) + SpacerVer(height = 4.dp) + Balance(balance = balance, color = contrastColor) + BalanceBaseCurrency(balanceBaseCurrency = balanceBaseCurrency, color = dynamicContrast) + } +} + +@Composable +private fun Header( + account: AccountUi, + color: Color, + dynamicContrast: Color, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + ItemIcon( + itemIcon = account.icon, + size = IconSize.M, + tint = color, + ) + SpacerHor(width = 8.dp) + B2( + modifier = Modifier.weight(1f), + text = account.name, + color = color, + fontWeight = FontWeight.ExtraBold + ) + if (account.excluded) { + SpacerHor(width = 4.dp) + Caption(text = stringResource(R.string.excluded), color = dynamicContrast) + } + } +} + +@Composable +private fun Balance( + balance: ValueUi, + color: Color, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AmountCurrency(value = balance, color = color) + } +} + +@Composable +private fun BalanceBaseCurrency( + balanceBaseCurrency: ValueUi?, + color: Color, + modifier: Modifier = Modifier, +) { + if (balanceBaseCurrency != null) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = 2.dp) + .padding(start = 14.dp), // so it looks aligned with the balance + verticalAlignment = Alignment.CenterVertically + ) { + AmountCurrencySmall(value = balanceBaseCurrency, color = color) + } + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + AccountCard( + account = dummyAccountUi(excluded = true), + balance = dummyValueUi("1,324.50"), + balanceBaseCurrency = dummyValueUi("2,972.95", "BGN") + ) { + + } + } +} +// endregion \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/components/AccountFolderCard.kt b/accounts/src/main/java/com/ivy/accounts/components/AccountFolderCard.kt new file mode 100644 index 0000000000..6ffe6e6bad --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/components/AccountFolderCard.kt @@ -0,0 +1,246 @@ +package com.ivy.accounts.components + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.accounts.R +import com.ivy.accounts.data.AccountListItemUi.AccountWithBalance +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.account.dummyFolderUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.core.ui.value.AmountCurrency +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.Blue +import com.ivy.design.l0_system.color.Red +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l0_system.color.rememberDynamicContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.isInPreview + + +@Composable +fun AccountFolderCard( + folder: FolderUi, + balance: ValueUi, + accounts: List, + accountsCount: Int, + modifier: Modifier = Modifier, + onAccountClick: (AccountUi) -> Unit, + onFolderClick: () -> Unit, +) { + val dynamicContrast = rememberDynamicContrast(folder.color) + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(UI.shapes.rounded) + .border(1.dp, dynamicContrast, UI.shapes.rounded) + .clickable(onClick = onFolderClick), + ) { + val contrastColor = rememberContrast(folder.color) + Column( + modifier = Modifier + .fillMaxWidth() + .background(folder.color, UI.shapes.roundedTop) + .padding(horizontal = 16.dp) + .padding(top = 4.dp, bottom = 12.dp) + ) { + IconNameRow(folderName = folder.name, folderIcon = folder.icon, color = contrastColor) + SpacerVer(height = 2.dp) + Balance(balance = balance, color = contrastColor) + } + var expanded by if (isInPreview()) remember { + mutableStateOf(previewExpanded) + } else remember { mutableStateOf(false) } + ExpandCollapse( + expanded = expanded, + color = UI.colorsInverted.pure, + accountsCount = accountsCount, + onSetExpanded = { expanded = it } + ) + Accounts(expanded = expanded, items = accounts, onClick = onAccountClick) + } +} + +@Composable +private fun IconNameRow( + folderName: String, + folderIcon: ItemIcon, + color: Color, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + ItemIcon( + itemIcon = folderIcon, + size = IconSize.M, + tint = color, + ) + SpacerHor(width = 8.dp) + B2( + modifier = Modifier.weight(1f), + text = folderName, + color = color, + fontWeight = FontWeight.ExtraBold + ) + } +} + +@Composable +private fun Balance( + balance: ValueUi, + color: Color, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AmountCurrency(value = balance, color = color) + } +} + +@Composable +private fun ExpandCollapse( + expanded: Boolean, + color: Color, + accountsCount: Int, + onSetExpanded: (Boolean) -> Unit +) { + if (accountsCount > 0) { + IvyButton( + size = ButtonSize.Big, + shape = UI.shapes.roundedBottom, + visibility = Visibility.Low, + feeling = Feeling.Custom(color), + text = if (expanded) + "Tap to collapse ($accountsCount)" else "Tap to expand ($accountsCount)", + icon = if (expanded) + R.drawable.ic_round_expand_less_24 else R.drawable.round_expand_more_24 + ) { + onSetExpanded(!expanded) + } + } else { + B2( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + text = "Empty folder", + fontWeight = FontWeight.ExtraBold, + color = UI.colors.neutral, + textAlign = TextAlign.Center + ) + } + +} + +@Composable +private fun Accounts( + expanded: Boolean, + items: List, + onClick: (AccountUi) -> Unit +) { + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + Column(Modifier.fillMaxWidth()) { + items.forEach { + key("${it.account.id}${it.balance.amount}") { + AccountCard( + account = it.account, + balance = it.balance, + balanceBaseCurrency = it.balanceBaseCurrency, + onClick = { onClick(it.account) } + ) + SpacerVer(height = 8.dp) + } + } + SpacerVer(height = 4.dp) + } + } +} + + +// region Preview +private var previewExpanded = false + +@Preview +@Composable +private fun Preview_Collapsed() { + ComponentPreview { + AccountFolderCard( + folder = dummyFolderUi("Business"), + balance = dummyValueUi("5,320.50"), + accounts = emptyList(), + accountsCount = 0, + onAccountClick = {}, + onFolderClick = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_Expanded() { + ComponentPreview { + previewExpanded = true + AccountFolderCard( + folder = dummyFolderUi("Business"), + balance = dummyValueUi("5,320.50"), + accounts = listOf( + AccountWithBalance( + account = dummyAccountUi("Account 1"), + balance = dummyValueUi("1,000.00", "ADA"), + balanceBaseCurrency = dummyValueUi("358.76") + ), + AccountWithBalance( + account = dummyAccountUi("Account 2", color = Blue, excluded = true), + balance = dummyValueUi("0.00"), + balanceBaseCurrency = null + ), + AccountWithBalance( + account = dummyAccountUi("Account 3", color = Red), + balance = dummyValueUi("4,320.50"), + balanceBaseCurrency = null + ), + ), + accountsCount = 3, + onAccountClick = {}, + onFolderClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/components/AccountsList.kt b/accounts/src/main/java/com/ivy/accounts/components/AccountsList.kt new file mode 100644 index 0000000000..f01ad2c19d --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/components/AccountsList.kt @@ -0,0 +1,133 @@ +package com.ivy.accounts.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.accounts.R +import com.ivy.accounts.data.AccountListItemUi +import com.ivy.accounts.data.AccountListItemUi.* +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +fun LazyListScope.accountsList( + items: List, + noAccounts: Boolean, + onAccountClick: (AccountUi) -> Unit, + onFolderClick: (FolderUi) -> Unit, + onCreateAccount: () -> Unit, +) { + items( + items = items, + key = { + when (it) { + is AccountWithBalance -> "acc_${it.account.id}" + is FolderWithAccounts -> "folder_${it.folder.id}" + is Archived -> "archived_accounts" + } + } + ) { item -> + when (item) { + is AccountWithBalance -> { + SpacerVer(height = 8.dp) + AccountCard( + account = item.account, + balance = item.balance, + balanceBaseCurrency = item.balanceBaseCurrency, + onClick = { onAccountClick(item.account) } + ) + } + is FolderWithAccounts -> { + SpacerVer(height = 8.dp) + AccountFolderCard( + folder = item.folder, + balance = item.balance, + accounts = item.accItems, + accountsCount = item.accountsCount, + onAccountClick = onAccountClick, + onFolderClick = { + onFolderClick(item.folder) + }, + ) + } + is Archived -> { + SpacerVer(height = 16.dp) + ArchivedAccounts(archived = item, onAccountClick = onAccountClick) + } + } + } + + if (noAccounts) { + item { + EmptyState(onCreateAccount = onCreateAccount) + } + } +} + +@Composable +private fun EmptyState( + onCreateAccount: () -> Unit +) { + SpacerVer(height = 96.dp) + B1( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = "No accounts", + textAlign = TextAlign.Center, + color = UI.colors.primary + ) + SpacerVer(height = 12.dp) + B2( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = "To use Ivy Wallet you need to create an account first.", + textAlign = TextAlign.Center + ) + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Focused, + feeling = Feeling.Positive, + text = "Create account", + icon = R.drawable.ic_vue_money_wallet, + onClick = onCreateAccount, + ) + SpacerVer(height = 24.dp) +} + + +// region Preview +@Preview +@Composable +private fun Preview_EmptyState() { + ComponentPreview { + LazyColumn { + accountsList( + items = emptyList(), + noAccounts = true, + onFolderClick = {}, + onCreateAccount = {}, + onAccountClick = {}, + ) + } + } +} +// endregion \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/components/ArchivedAccounts.kt b/accounts/src/main/java/com/ivy/accounts/components/ArchivedAccounts.kt new file mode 100644 index 0000000000..221e92d870 --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/components/ArchivedAccounts.kt @@ -0,0 +1,125 @@ +package com.ivy.accounts.components + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.* +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.accounts.R +import com.ivy.accounts.data.AccountListItemUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.design.l0_system.color.Blue +import com.ivy.design.l0_system.color.Red +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.isInPreview + +@Composable +internal fun ArchivedAccounts( + archived: AccountListItemUi.Archived, + onAccountClick: (AccountUi) -> Unit, +) { + var expanded by if (isInPreview()) remember { + mutableStateOf(previewExpanded) + } else remember { mutableStateOf(false) } + ArchivedDivider( + expanded = expanded, + accountsCount = archived.accountsCount, + onSetExpanded = { expanded = it } + ) + AccountsList( + accounts = archived.accHolders, + expanded = expanded, + onAccountClick = onAccountClick + ) +} + +@Composable +private fun ArchivedDivider( + expanded: Boolean, + accountsCount: Int, + onSetExpanded: (Boolean) -> Unit +) { + IvyButton( + size = ButtonSize.Big, + visibility = Visibility.Low, + feeling = Feeling.Neutral, + text = "Archived ($accountsCount)", + icon = if (expanded) + R.drawable.round_expand_more_24 else R.drawable.ic_round_expand_less_24 + ) { + onSetExpanded(!expanded) + } +} + +@Composable +private fun AccountsList( + accounts: List, + expanded: Boolean, + onAccountClick: (AccountUi) -> Unit, +) { + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column { + accounts.forEach { item -> + key("archived_${item.account.id}") { + SpacerVer(height = 12.dp) + AccountCard( + account = item.account, + balance = item.balance, + balanceBaseCurrency = item.balanceBaseCurrency + ) { + onAccountClick(item.account) + } + } + } + } + } +} + + +// region Preview +private var previewExpanded = false + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + previewExpanded = true + Column { + ArchivedAccounts( + archived = AccountListItemUi.Archived( + accHolders = listOf( + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Account 1"), + balance = dummyValueUi("1,000.00", "BGN"), + balanceBaseCurrency = dummyValueUi("500") + ), + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Account 2", color = Blue, excluded = true), + balance = dummyValueUi("0.00"), + balanceBaseCurrency = null + ), + AccountListItemUi.AccountWithBalance( + account = dummyAccountUi("Account 3", color = Red), + balance = dummyValueUi("4,320.50"), + balanceBaseCurrency = null + ), + ), + accountsCount = 3, + ), + onAccountClick = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/data/AccountListItemUi.kt b/accounts/src/main/java/com/ivy/accounts/data/AccountListItemUi.kt new file mode 100644 index 0000000000..fe562cac9b --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/data/AccountListItemUi.kt @@ -0,0 +1,30 @@ +package com.ivy.accounts.data + +import androidx.compose.runtime.Immutable +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.FolderUi + +@Immutable +sealed interface AccountListItemUi { + @Immutable + data class AccountWithBalance( + val account: AccountUi, + val balance: ValueUi, + val balanceBaseCurrency: ValueUi?, + ) : AccountListItemUi + + @Immutable + data class FolderWithAccounts( + val folder: FolderUi, + val accItems: List, + val accountsCount: Int, + val balance: ValueUi, + ) : AccountListItemUi + + @Immutable + data class Archived( + val accHolders: List, + val accountsCount: Int, + ) : AccountListItemUi +} \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/modal/CreateModal.kt b/accounts/src/main/java/com/ivy/accounts/modal/CreateModal.kt new file mode 100644 index 0000000000..42b27ccc42 --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/modal/CreateModal.kt @@ -0,0 +1,85 @@ +package com.ivy.accounts.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.accounts.R +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview + +@Composable +internal fun BoxScope.CreateModal( + modal: IvyModal, + onCreateAccount: () -> Unit, + onCreateFolder: () -> Unit, +) { + Modal(modal = modal, actions = {}) { + Title(text = stringResource(R.string.create)) + SpacerVer(height = 24.dp) + FolderButton { + modal.hide() + onCreateFolder() + } + SpacerVer(height = 12.dp) + AccountButton { + modal.hide() + onCreateAccount() + } + SpacerVer(height = 24.dp) + } +} + +@Composable +private fun FolderButton(onClick: () -> Unit) { + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "New folder", + icon = R.drawable.ic_vue_files_folder, + onClick = onClick, + ) +} + +@Composable +private fun AccountButton(onClick: () -> Unit) { + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.High, + feeling = Feeling.Positive, + text = "New account", + icon = R.drawable.ic_custom_account_s, + onClick = onClick, + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + CreateModal( + modal = modal, + onCreateAccount = {}, + onCreateFolder = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/modal/NetWorthInfoModal.kt b/accounts/src/main/java/com/ivy/accounts/modal/NetWorthInfoModal.kt new file mode 100644 index 0000000000..25b5c685a6 --- /dev/null +++ b/accounts/src/main/java/com/ivy/accounts/modal/NetWorthInfoModal.kt @@ -0,0 +1,87 @@ +package com.ivy.accounts.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.value.AmountCurrency +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B1Second +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Body +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview + +@Composable +internal fun BoxScope.NetWorthInfoModal( + modal: IvyModal, + totalBalance: ValueUi, + availableBalance: ValueUi, + excludedBalance: ValueUi, +) { + Modal( + modal = modal, + actions = { + Positive(text = "Got it") { + modal.hide() + } + } + ) { + Title(text = "Net-worth") + SpacerVer(height = 4.dp) + Body( + text = "Your net-worth is the combined value of all your assets" + + " minus your liabilities." + ) + SpacerVer(height = 24.dp) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + B2(text = "Available balance") + Row { + AmountCurrency(availableBalance) + } + B1Second(text = "+") + B2(text = "Excluded balance") + Row { + AmountCurrency(excludedBalance, color = UI.colors.red) + } + B1Second(text = "=") + B2(text = "Net-worth") + Row { + AmountCurrency(totalBalance, color = UI.colors.primary) + } + } + SpacerVer(height = 24.dp) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + NetWorthInfoModal( + modal = modal, + totalBalance = dummyValueUi("203k"), + availableBalance = dummyValueUi("136,3k"), + excludedBalance = dummyValueUi("64,3k"), + ) + } +} +// endregion \ No newline at end of file diff --git a/android-notifications/build.gradle.kts b/android-notifications/build.gradle.kts index 3c5ac86a86..b39da6d93c 100644 --- a/android-notifications/build.gradle.kts +++ b/android-notifications/build.gradle.kts @@ -12,7 +12,6 @@ plugins { dependencies { Hilt() implementation(project(":common:main")) - implementation(project(":app-base")) implementation(project(":core:ui")) AndroidX(api = false) } \ No newline at end of file diff --git a/android-notifications/src/main/java/com/ivy/notifications/NotificationService.kt b/android-notifications/src/main/java/com/ivy/notifications/NotificationService.kt index 67ec7bcc26..7ee7fbf54a 100644 --- a/android-notifications/src/main/java/com/ivy/notifications/NotificationService.kt +++ b/android-notifications/src/main/java/com/ivy/notifications/NotificationService.kt @@ -5,7 +5,7 @@ import android.content.Context import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat -import com.ivy.base.R +import com.ivy.resources.R class NotificationService( private val context: Context diff --git a/app-base/src/main/java/com/ivy/base/ClosedTimeRange.kt b/app-base/src/main/java/com/ivy/base/ClosedTimeRangeOld.kt similarity index 71% rename from app-base/src/main/java/com/ivy/base/ClosedTimeRange.kt rename to app-base/src/main/java/com/ivy/base/ClosedTimeRangeOld.kt index a578fccbd6..f8fb2fa414 100644 --- a/app-base/src/main/java/com/ivy/base/ClosedTimeRange.kt +++ b/app-base/src/main/java/com/ivy/base/ClosedTimeRangeOld.kt @@ -4,17 +4,18 @@ import com.ivy.common.time.beginningOfIvyTime import com.ivy.common.time.timeNow import java.time.LocalDateTime -data class ClosedTimeRange( +@Deprecated("old!") +data class ClosedTimeRangeOld( val from: LocalDateTime, val to: LocalDateTime ) { companion object { - fun allTimeIvy(): ClosedTimeRange = ClosedTimeRange( + fun allTimeIvy(): ClosedTimeRangeOld = ClosedTimeRangeOld( from = beginningOfIvyTime(), to = timeNow() ) - fun to(to: LocalDateTime): ClosedTimeRange = ClosedTimeRange( + fun to(to: LocalDateTime): ClosedTimeRangeOld = ClosedTimeRangeOld( from = beginningOfIvyTime(), to = to ) diff --git a/app-base/src/main/java/com/ivy/base/FromToTimeRange.kt b/app-base/src/main/java/com/ivy/base/FromToTimeRange.kt index 6d1708e6bd..cdaec6273d 100644 --- a/app-base/src/main/java/com/ivy/base/FromToTimeRange.kt +++ b/app-base/src/main/java/com/ivy/base/FromToTimeRange.kt @@ -1,7 +1,7 @@ package com.ivy.base -import com.ivy.common.time.* -import com.ivy.data.transaction.TransactionOld +import com.ivy.common.time.formatDateOnly +import com.ivy.common.time.timeNow import java.time.LocalDateTime data class FromToTimeRange( @@ -14,21 +14,6 @@ data class FromToTimeRange( fun to(): LocalDateTime = to ?: timeNow().plusYears(30) - fun upcomingFrom(): LocalDateTime { - val startOfDayNowUTC = - startOfDayNowUTC().minusDays(1) //-1 day to ensure that everything is included - return if (includes(startOfDayNowUTC)) startOfDayNowUTC else from() - } - - fun overdueTo(): LocalDateTime { - val startOfDayNowUTC = - startOfDayNowUTC().plusDays(1) //+1 day to ensure that everything is included - return if (includes(startOfDayNowUTC)) startOfDayNowUTC else to() - } - - fun includes(dateTime: LocalDateTime): Boolean = - dateTime.isAfter(from()) && dateTime.isBefore(to()) - fun toDisplay(): String { return when { from != null && to != null -> { @@ -46,35 +31,3 @@ data class FromToTimeRange( } } } - -fun Iterable.filterUpcoming(): List { - val todayStartOfDayUTC = dateNowUTC().atStartOfDay() - - return filter { - //make sure that it's in the future - it.dueDate != null && it.dueDate!!.isAfter(todayStartOfDayUTC) - } -} - -fun Iterable.filterOverdue(): List { - val todayStartOfDayUTC = dateNowUTC().atStartOfDay() - - return filter { - //make sure that it's in the past - it.dueDate != null && it.dueDate!!.isBefore(todayStartOfDayUTC) - } -} - -fun FromToTimeRange.toCloseTimeRangeUnsafe(): ClosedTimeRange { - return ClosedTimeRange( - from = from(), - to = to() - ) -} - -fun FromToTimeRange.toCloseTimeRange(): ClosedTimeRange { - return ClosedTimeRange( - from = from ?: beginningOfIvyTime(), - to = to ?: endOfIvyTime() - ) -} \ No newline at end of file diff --git a/app-locked/build.gradle.kts b/app-locked/build.gradle.kts index c1bb64e050..e96030d5c7 100644 --- a/app-locked/build.gradle.kts +++ b/app-locked/build.gradle.kts @@ -12,8 +12,6 @@ dependencies { implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":app-base")) implementation(project(":core:ui")) - implementation(project(":ui-components-old")) implementation(project(":core:data-model")) } \ No newline at end of file diff --git a/app-locked/src/main/java/com/ivy/locked/AppLockedScreen.kt b/app-locked/src/main/java/com/ivy/locked/AppLockedScreen.kt index 5af9d7422b..b7d35c76d7 100644 --- a/app-locked/src/main/java/com/ivy/locked/AppLockedScreen.kt +++ b/app-locked/src/main/java/com/ivy/locked/AppLockedScreen.kt @@ -1,6 +1,7 @@ -package com.ivy.wallet.ui.applocked +package com.ivy.locked +import android.app.KeyguardManager import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -16,17 +17,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.ivy.base.R import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.Gray import com.ivy.design.l0_system.style +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.utils.hasLockScreen @Composable fun BoxWithConstraintsScope.AppLockedScreen( @@ -77,16 +77,12 @@ fun BoxWithConstraintsScope.AppLockedScreen( val context = LocalContext.current IvyButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Focused, + feeling = Feeling.Positive, text = stringResource(R.string.unlock), - textStyle = UI.typo.b2.style( - color = White, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ), - wrapContentMode = false + icon = null, ) { osAuthentication( context = context, @@ -94,6 +90,7 @@ fun BoxWithConstraintsScope.AppLockedScreen( onContinueWithoutAuthentication = onContinueWithoutAuthentication ) } + Spacer(Modifier.height(24.dp)) //To automatically launch the biometric screen on load of this composable @@ -119,6 +116,11 @@ private fun osAuthentication( } } +private fun hasLockScreen(context: Context): Boolean { + val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + return keyguardManager.isDeviceSecure +} + @Preview @Composable private fun Preview_Locked() { diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5736f62b60..d48173620f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,9 +50,8 @@ android { buildTypes { release { - //TODO: R8 disabled until `modularization` is stable - isMinifyEnabled = false - isShrinkResources = false + isMinifyEnabled = true + isShrinkResources = true isDebuggable = false isDefault = false @@ -65,6 +64,23 @@ android { resValue("string", "app_name", "Ivy Wallet") } + create("demo") { + isMinifyEnabled = true + isShrinkResources = true + isDebuggable = false + isDefault = false + + signingConfig = signingConfigs.getByName("debug") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + + applicationIdSuffix = ".debug" + matchingFallbacks.add("release") + resValue("string", "app_name", "Ivy Wallet Demo") + } + debug { isDebuggable = true isMinifyEnabled = false @@ -126,38 +142,20 @@ android { dependencies { implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":app-base")) implementation(project(":core:ui")) implementation(project(":navigation")) -// implementation(project(":budgets")) -// implementation(project(":categories")) -// implementation(project(":loans")) -// implementation(project(":onboarding")) -// implementation(project(":pie-charts")) -// implementation(project(":planned-payments")) -// implementation(project(":reports")) -// implementation(project(":settings")) -// implementation(project(":search-transactions")) -// implementation(project(":transaction-details")) + implementation(project(":categories")) + implementation(project(":settings")) + implementation(project(":transaction")) implementation(project(":core:data-model")) implementation(project(":widgets")) - implementation(project(":main")) + implementation(project(":main:impl")) implementation(project(":app-locked")) -// implementation(project(":balance-prediction")) -// implementation(project(":donate")) -// implementation(project(":item-transactions")) - implementation(project(":web-view")) -// implementation(project(":settings")) -// implementation(project(":import-csv-backup")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":temp-network")) implementation(project(":billing")) implementation(project(":android-notifications")) implementation(project(":core:exchange-provider")) implementation(project(":core:domain")) implementation(project(":debug")) - implementation(project(":navigation")) implementation(project(":onboarding")) Hilt() diff --git a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/123.json b/app/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/123.json similarity index 97% rename from temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/123.json rename to app/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/123.json index 7a8674225d..1165500a8a 100644 --- a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/123.json +++ b/app/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/123.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 123, - "identityHash": "8b1b205631a16c4cd4b616d7bc3983b3", + "identityHash": "53cba3d6595ca41b4f6966577609da7c", "entities": [ { "tableName": "accounts", @@ -190,7 +190,7 @@ }, { "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `parentCategoryId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "name", @@ -216,12 +216,6 @@ "affinity": "REAL", "notNull": true }, - { - "fieldPath": "parentCategoryId", - "columnName": "parentCategoryId", - "affinity": "TEXT", - "notNull": false - }, { "fieldPath": "isSynced", "columnName": "isSynced", @@ -462,7 +456,7 @@ }, { "tableName": "exchange_rates", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseCurrency` TEXT NOT NULL, `currency` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`baseCurrency`, `currency`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseCurrency` TEXT NOT NULL, `currency` TEXT NOT NULL, `rate` REAL NOT NULL, `manualOverride` INTEGER NOT NULL, PRIMARY KEY(`baseCurrency`, `currency`))", "fields": [ { "fieldPath": "baseCurrency", @@ -481,6 +475,12 @@ "columnName": "rate", "affinity": "REAL", "notNull": true + }, + { + "fieldPath": "manualOverride", + "columnName": "manualOverride", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -707,7 +707,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8b1b205631a16c4cd4b616d7bc3983b3')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '53cba3d6595ca41b4f6966577609da7c')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/ivy/wallet/IvyAppTestRunner.kt b/app/src/androidTest/java/com/ivy/wallet/IvyAppTestRunner.kt index 96389855a9..6344354a38 100644 --- a/app/src/androidTest/java/com/ivy/wallet/IvyAppTestRunner.kt +++ b/app/src/androidTest/java/com/ivy/wallet/IvyAppTestRunner.kt @@ -2,11 +2,7 @@ package com.ivy.wallet import android.app.Application import android.content.Context -import android.content.Intent import androidx.test.runner.AndroidJUnitRunner -import com.ivy.base.RootIntent -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.wallet.ui.RootActivity import dagger.hilt.android.testing.HiltTestApplication @Suppress("UNUSED") @@ -15,14 +11,6 @@ class IvyAppTestRunner : AndroidJUnitRunner() { override fun newApplication(cl: ClassLoader?, name: String?, context: Context): Application { IvyAndroidApp.appContext = context com.ivy.core.ui.temp.GlobalProvider.appContext = context - com.ivy.core.ui.temp.GlobalProvider.rootIntent = object : RootIntent { - override fun getIntent(context: Context): Intent = - Intent(context, RootActivity::class.java) - - override fun addTransactionStart(context: Context, type: TrnTypeOld): Intent = - Intent(context, RootActivity::class.java).apply { - } - } return super.newApplication(cl, HiltTestApplication::class.java.name, context) } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/AppModuleDI.kt b/app/src/main/java/com/ivy/wallet/AppModuleDI.kt index d1877b91a9..9fc6a713f0 100644 --- a/app/src/main/java/com/ivy/wallet/AppModuleDI.kt +++ b/app/src/main/java/com/ivy/wallet/AppModuleDI.kt @@ -1,354 +1,18 @@ package com.ivy.wallet import android.content.Context -import com.google.gson.Gson -import com.google.gson.GsonBuilder import com.ivy.billing.IvyBilling -import com.ivy.core.ui.temp.trash.IvyWalletCtx import com.ivy.notifications.NotificationService -import com.ivy.temp.persistence.ExchangeRateDao -import com.ivy.wallet.domain.deprecated.logic.notification.TransactionReminderLogic -import com.ivy.wallet.domain.deprecated.logic.zip.ExportZipLogic -import com.ivy.wallet.domain.deprecated.sync.IvySync -import com.ivy.wallet.domain.deprecated.sync.item.* -import com.ivy.wallet.domain.deprecated.sync.uploader.* -import com.ivy.wallet.domain.pure.data.WalletDAOs -import com.ivy.wallet.io.network.ErrorCodeTypeAdapter -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.LocalDateTimeTypeAdapter -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.error.ErrorCode -import com.ivy.wallet.io.network.service.AccountService -import com.ivy.wallet.io.network.service.CategoryService -import com.ivy.wallet.io.network.service.TransactionService -import com.ivy.wallet.io.persistence.IvyRoomDatabase -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import java.time.LocalDateTime import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object AppModuleDI { - @Provides - @Singleton - fun provideIvyContext(): IvyWalletCtx { - return IvyWalletCtx() - } - - @Provides - @Singleton - fun provideSharedPrefs( - @ApplicationContext appContext: Context, - ): SharedPrefs { - return SharedPrefs(appContext) - } - - @Provides - @Singleton - fun provideIvySession(sharedPrefs: SharedPrefs, userDao: UserDao): IvySession { - return IvySession(sharedPrefs, userDao) - } - - @Provides - @Singleton - fun provideGson(): Gson { - return GsonBuilder() - .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeTypeAdapter()) - .registerTypeAdapter(ErrorCode::class.java, ErrorCodeTypeAdapter()) - .create() - } - - @Provides - @Singleton - fun provideRestClient( - @ApplicationContext appContext: Context, - gson: Gson, - ivySession: IvySession - ): RestClient { - return RestClient.initialize(appContext, ivySession, gson) - } - - @Provides - @Singleton - fun provideIvyRoomDatabase( - @ApplicationContext appContext: Context, - ): IvyRoomDatabase { - return IvyRoomDatabase.create( - applicationContext = appContext, - ) - } - - @Provides - fun provideUserDao(db: IvyRoomDatabase): UserDao = db.userDao() - - @Provides - fun provideAccountDao(db: IvyRoomDatabase): AccountDao = db.accountDao() - - @Provides - fun provideTransactionDao(db: IvyRoomDatabase): TransactionDao = db.transactionDao() - - @Provides - fun provideCategoryDao(db: IvyRoomDatabase): CategoryDao = db.categoryDao() - - @Provides - fun provideBudgetDao(db: IvyRoomDatabase): BudgetDao = db.budgetDao() - - @Provides - fun provideSettingsDao(db: IvyRoomDatabase): SettingsDao = db.settingsDao() - - @Provides - fun provideLoanDao(db: IvyRoomDatabase): LoanDao = db.loanDao() - - @Provides - fun provideLoanRecordDao(db: IvyRoomDatabase): LoanRecordDao = db.loanRecordDao() - - @Provides - fun provideTrnRecurringRuleDao(db: IvyRoomDatabase): PlannedPaymentRuleDao = - db.plannedPaymentRuleDao() - - //Sync - @Provides - fun provideAccountUploader( - accountDao: AccountDao, - transactionDao: TransactionDao, - restClient: RestClient, - ivySession: IvySession - ): AccountUploader { - return AccountUploader( - accountDao = accountDao, - transactionDao = transactionDao, - restClient = restClient, - ivySession = ivySession - ) - } - - @Provides - fun provideAccountSync( - sharedPrefs: SharedPrefs, - dao: AccountDao, - restClient: RestClient, - uploader: AccountUploader, - ivySession: IvySession - ): AccountSync { - return AccountSync( - sharedPrefs = sharedPrefs, - dao = dao, - restClient = restClient, - uploader = uploader, - ivySession = ivySession - ) - } - - @Provides - fun provideCategoryUploader( - categoryDao: CategoryDao, - restClient: RestClient, - ivySession: IvySession - ): CategoryUploader { - return CategoryUploader( - dao = categoryDao, - restClient = restClient, - ivySession = ivySession - ) - } - - @Provides - fun provideBudgetUploader( - budgetDao: BudgetDao, - restClient: RestClient, - ivySession: IvySession - ): BudgetUploader { - return BudgetUploader( - dao = budgetDao, - restClient = restClient, - ivySession = ivySession - ) - } - - @Provides - fun provideLoanUploader( - loanDao: LoanDao, - restClient: RestClient, - ivySession: IvySession - ): LoanUploader { - return LoanUploader( - dao = loanDao, - restClient = restClient, - ivySession = ivySession - ) - } - - @Provides - fun provideLoanRecordUploader( - dao: LoanRecordDao, - restClient: RestClient, - ivySession: IvySession - ): LoanRecordUploader { - return LoanRecordUploader( - dao = dao, - restClient = restClient, - ivySession = ivySession - ) - } - - @Provides - fun provideCategorySync( - sharedPrefs: SharedPrefs, - categoryDao: CategoryDao, - restClient: RestClient, - categoryUploader: CategoryUploader, - ivySession: IvySession - ): CategorySync { - return CategorySync( - sharedPrefs = sharedPrefs, - dao = categoryDao, - restClient = restClient, - uploader = categoryUploader, - ivySession = ivySession - ) - } - - @Provides - fun provideBudgetSync( - sharedPrefs: SharedPrefs, - budgetDao: BudgetDao, - restClient: RestClient, - budgetUploader: BudgetUploader, - ivySession: IvySession - ): BudgetSync { - return BudgetSync( - sharedPrefs = sharedPrefs, - dao = budgetDao, - restClient = restClient, - uploader = budgetUploader, - ivySession = ivySession - ) - } - - @Provides - fun provideLoanSync( - sharedPrefs: SharedPrefs, - dao: LoanDao, - restClient: RestClient, - loanUploader: LoanUploader, - ivySession: IvySession - ): LoanSync { - return LoanSync( - sharedPrefs = sharedPrefs, - dao = dao, - restClient = restClient, - uploader = loanUploader, - ivySession = ivySession - ) - } - - @Provides - fun provideLoanRecordSync( - sharedPrefs: SharedPrefs, - dao: LoanRecordDao, - restClient: RestClient, - uploader: LoanRecordUploader, - ivySession: IvySession - ): LoanRecordSync { - return LoanRecordSync( - sharedPrefs = sharedPrefs, - dao = dao, - restClient = restClient, - uploader = uploader, - ivySession = ivySession - ) - } - - @Provides - fun provideTransactionUploader( - transactionDao: TransactionDao, - restClient: RestClient, - ivySession: IvySession - ): TransactionUploader { - return TransactionUploader( - dao = transactionDao, - restClient = restClient, - ivySession = ivySession - ) - } - - @Provides - fun provideTransactionSync( - sharedPrefs: SharedPrefs, - transactionDao: TransactionDao, - restClient: RestClient, - transactionUploader: TransactionUploader, - ivySession: IvySession - ): TransactionSync { - return TransactionSync( - sharedPrefs = sharedPrefs, - dao = transactionDao, - restClient = restClient, - uploader = transactionUploader, - ivySession = ivySession - ) - } - - @Provides - fun providePlannedPaymentRuleUploader( - plannedPaymentRuleDao: PlannedPaymentRuleDao, - restClient: RestClient, - ivySession: IvySession - ): PlannedPaymentRuleUploader { - return PlannedPaymentRuleUploader( - dao = plannedPaymentRuleDao, - restClient = restClient, - ivySession = ivySession - ) - } - - @Provides - fun providePlannedPaymentSync( - sharedPrefs: SharedPrefs, - plannedPaymentRuleDao: PlannedPaymentRuleDao, - restClient: RestClient, - plannedPaymentRuleUploader: PlannedPaymentRuleUploader, - ivySession: IvySession - ): PlannedPaymentSync { - return PlannedPaymentSync( - sharedPrefs = sharedPrefs, - dao = plannedPaymentRuleDao, - restClient = restClient, - uploader = plannedPaymentRuleUploader, - ivySession = ivySession - ) - } - - @Provides - @Singleton - fun provideIvySync( - accountSync: AccountSync, - categorySync: CategorySync, - transactionSync: TransactionSync, - plannedPaymentSync: PlannedPaymentSync, - budgetSync: BudgetSync, - loanSync: LoanSync, - loanRecordSync: LoanRecordSync, - ivySession: IvySession - ): IvySync { - return IvySync( - accountSync = accountSync, - categorySync = categorySync, - transactionSync = transactionSync, - plannedPaymentSync = plannedPaymentSync, - budgetSync = budgetSync, - loanSync = loanSync, - loanRecordSync = loanRecordSync, - ivySession = ivySession - ) - } - @Provides @Singleton fun provideIvyBilling( @@ -362,75 +26,4 @@ object AppModuleDI { ): NotificationService { return NotificationService(appContext) } - - @Provides - fun provideTransactionReminderLogic( - @ApplicationContext appContext: Context, - sharedPrefs: SharedPrefs, - ): TransactionReminderLogic { - return TransactionReminderLogic( - appContext = appContext, - sharedPrefs = sharedPrefs - ) - } - - @Provides - fun provideExchangeRatesDao( - roomDatabase: IvyRoomDatabase - ): ExchangeRateDao { - return roomDatabase.exchangeRatesDao() - } - - @Provides - fun provideWalletDAOs( - accountDao: AccountDao, - transactionDao: TransactionDao, - exchangeRateDao: ExchangeRateDao - ): WalletDAOs { - return WalletDAOs( - accountDao = accountDao, - transactionDao = transactionDao, - exchangeRateDao = exchangeRateDao - ) - } - - @Provides - fun providesExportZipLogic( - accountDao: AccountDao, - budgetDao: BudgetDao, - categoryDao: CategoryDao, - loanRecordDao: LoanRecordDao, - loanDao: LoanDao, - plannedPaymentRuleDao: PlannedPaymentRuleDao, - settingsDao: SettingsDao, - transactionDao: TransactionDao, - sharedPrefs: SharedPrefs - ): ExportZipLogic { - return ExportZipLogic( - accountDao, - budgetDao, - categoryDao, - loanRecordDao, - loanDao, - plannedPaymentRuleDao, - settingsDao, - transactionDao, - sharedPrefs - ) - } - - @Provides - fun provideAccountService( - restClient: RestClient - ): AccountService = restClient.accountService - - @Provides - fun provideCategoryService( - restClient: RestClient - ): CategoryService = restClient.categoryService - - @Provides - fun provideTransactionService( - restClient: RestClient - ): TransactionService = restClient.transactionService } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/IvyAndroidApp.kt b/app/src/main/java/com/ivy/wallet/IvyAndroidApp.kt index 930763f5f1..61932ad7ef 100644 --- a/app/src/main/java/com/ivy/wallet/IvyAndroidApp.kt +++ b/app/src/main/java/com/ivy/wallet/IvyAndroidApp.kt @@ -3,13 +3,9 @@ package com.ivy.wallet import android.annotation.SuppressLint import android.app.Application import android.content.Context -import android.content.Intent import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration -import com.ivy.base.RootIntent import com.ivy.common.BuildConfig -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.wallet.ui.RootActivity import dagger.hilt.android.HiltAndroidApp import timber.log.Timber import timber.log.Timber.DebugTree @@ -39,15 +35,6 @@ class IvyAndroidApp : Application(), Configuration.Provider { super.onCreate() appContext = this com.ivy.core.ui.temp.GlobalProvider.appContext = this - com.ivy.core.ui.temp.GlobalProvider.rootIntent = object : RootIntent { - override fun getIntent(context: Context): Intent = - Intent(context, RootActivity::class.java) - - override fun addTransactionStart(context: Context, type: TrnTypeOld): Intent = - Intent(context, RootActivity::class.java).apply { -// putExtra(RootViewModel.EXTRA_ADD_TRANSACTION_TYPE, type) - } - } if (BuildConfig.DEBUG) { Timber.plant(DebugTree()) diff --git a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt index a90a31ef46..6e6d650a58 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt @@ -22,6 +22,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.hilt.navigation.compose.hiltViewModel @@ -31,26 +33,33 @@ import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.common.api.ApiException import com.google.android.gms.tasks.Task import com.google.android.play.core.review.ReviewManagerFactory -import com.ivy.base.R +import com.ivy.categories.CategoriesScreen import com.ivy.common.Constants import com.ivy.common.Constants.SUPPORT_EMAIL -import com.ivy.common.time.TimeProvider +import com.ivy.common.time.provider.TimeProvider import com.ivy.common.time.timeNow import com.ivy.common.time.toEpochMilli import com.ivy.core.ui.temp.RootScreen import com.ivy.debug.TestScreen import com.ivy.design.api.IvyUI -import com.ivy.main.MainScreen +import com.ivy.design.api.setAppDesign +import com.ivy.design.api.systems.ivyWalletDesign +import com.ivy.main.impl.MainScreen import com.ivy.navigation.NavigationRoot import com.ivy.navigation.Navigator import com.ivy.navigation.graph.DebugScreens import com.ivy.navigation.graph.OnboardingScreens import com.ivy.navigation.graph.TransactionScreens import com.ivy.onboarding.screen.debug.OnboardingDebug +import com.ivy.resources.R +import com.ivy.settings.SettingsScreen +import com.ivy.transaction.create.transfer.NewTransferScreen +import com.ivy.transaction.create.trn.NewTransactionScreen +import com.ivy.transaction.edit.transfer.EditTransferScreen +import com.ivy.transaction.edit.trn.EditTransactionScreen import com.ivy.wallet.BuildConfig import com.ivy.wallet.utils.activityForResultLauncher import com.ivy.wallet.utils.simpleActivityForResultLauncher -import com.ivy.widgets.AddTransactionWidget import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber import java.time.LocalDate @@ -58,7 +67,6 @@ import java.time.LocalTime import java.util.* import javax.inject.Inject - @AndroidEntryPoint class RootActivity : AppCompatActivity(), RootScreen { @@ -68,6 +76,12 @@ class RootActivity : AppCompatActivity(), RootScreen { @Inject lateinit var timeProvider: TimeProvider + /** + * Uncomment below code to use gDrive feature + */ +// @Inject +// lateinit var googleDriveService: GoogleDriveService + private lateinit var googleSignInLauncher: ActivityResultLauncher private lateinit var onGoogleSignInIdTokenResult: (idToken: String?) -> Unit @@ -88,17 +102,23 @@ class RootActivity : AppCompatActivity(), RootScreen { // Make the app drawing area fullscreen (draw behind status and nav bars) WindowCompat.setDecorFitsSystemWindows(window, false) - AddTransactionWidget.updateBroadcast(this) setContent { val viewModel: RootViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() val isSystemInDarkTheme = isSystemInDarkTheme() - LaunchedEffect(isSystemInDarkTheme) { + LaunchedEffect(state.theme, isSystemInDarkTheme) { + setAppDesign( + ivyWalletDesign( + theme = state.theme, + isSystemInDarkTheme = isSystemInDarkTheme + ) + ) } IvyUI { - NavigationRoot(viewModel) + NavigationRoot(state) } } @@ -106,7 +126,7 @@ class RootActivity : AppCompatActivity(), RootScreen { } @Composable - private fun BoxWithConstraintsScope.NavigationRoot(viewModel: RootViewModel) { + private fun BoxWithConstraintsScope.NavigationRoot(state: RootState) { NavigationRoot( navigator = navigator, onboardingScreens = OnboardingScreens( @@ -118,13 +138,15 @@ class RootActivity : AppCompatActivity(), RootScreen { addCategories = {} ), main = { MainScreen(it) }, + categories = { CategoriesScreen() }, + settings = { SettingsScreen() }, transactionScreens = TransactionScreens( accountTransactions = {}, categoryTransactions = {}, - newTransaction = {}, - newTransfer = {}, - transaction = {}, - transfer = {} + newTransaction = { NewTransactionScreen(arg = it) }, + newTransfer = { NewTransferScreen() }, + transaction = { EditTransactionScreen(trnId = it) }, + transfer = { EditTransferScreen(batchId = it) } ), debugScreens = DebugScreens( test = { TestScreen() } @@ -133,6 +155,12 @@ class RootActivity : AppCompatActivity(), RootScreen { } private fun setupActivityForResultLaunchers() { + + /** + * Uncomment below code to use gDrive feature + */ +// requestSignIn() + googleSignInLauncher() createFileLauncher() @@ -173,6 +201,23 @@ class RootActivity : AppCompatActivity(), RootScreen { // } } + /** + * Uncomment below code to use gDrive feature + */ +// private fun requestSignIn() { +// val signInOptions = googleDriveService.requestSignIn() +// val client = GoogleSignIn.getClient(this, signInOptions) +// +// Timber.d("Sign In Requested") +// // The result of the sign-in Intent is handled in onActivityResult. +// val launcher = registerForActivityResult( +// ActivityResultContracts.StartActivityForResult() +// ) { +// googleDriveService.handleSignInResult(this@RootActivity) +// } +// launcher.launch(client.signInIntent) +// } + private fun createFileLauncher() { createFileLauncher = activityForResultLauncher( createIntent = { _, fileName -> diff --git a/app/src/main/java/com/ivy/wallet/ui/RootState.kt b/app/src/main/java/com/ivy/wallet/ui/RootState.kt index 79d2718950..93c93be9b1 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootState.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootState.kt @@ -1,8 +1,10 @@ package com.ivy.wallet.ui import androidx.compose.runtime.Immutable +import com.ivy.data.Theme @Immutable data class RootState( - val appLocked: Boolean + val appLocked: Boolean, + val theme: Theme, ) \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt index b585effc16..6b44ed5b9f 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt @@ -1,28 +1,51 @@ package com.ivy.wallet.ui +import com.ivy.common.isNotEmpty import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.action.exchange.SyncExchangeRatesAct +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow +import com.ivy.core.domain.action.settings.theme.ThemeFlow +import com.ivy.data.CurrencyCode +import com.ivy.data.Theme import com.ivy.navigation.Navigator import com.ivy.navigation.destinations.Destination import com.ivy.onboarding.action.OnboardingFinishedAct import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import timber.log.Timber import javax.inject.Inject @HiltViewModel class RootViewModel @Inject constructor( private val onboardingFinishedAct: OnboardingFinishedAct, private val navigator: Navigator, -) : FlowViewModel() { - override fun initialState() = RootState(appLocked = false) + private val syncExchangeRatesAct: SyncExchangeRatesAct, + baseCurrencyFlow: BaseCurrencyFlow, + private val themeFlow: ThemeFlow, +) : FlowViewModel() { + override val initialState = InternalState(baseCurrency = "") - override fun initialUiState() = initialState() + override val stateFlow: Flow = baseCurrencyFlow().map { baseCurrency -> + if (baseCurrency.isNotEmpty()) { + Timber.i("Syncing exchange rates for $baseCurrency") + syncExchangeRatesAct(baseCurrency) + } + InternalState(baseCurrency = baseCurrency) + } - override fun stateFlow(): Flow = flow {} + override val initialUi = RootState(appLocked = false, theme = Theme.Auto) + + override val uiFlow: Flow = themeFlow(Unit).map { theme -> + RootState( + appLocked = false, + theme = theme + ) + } - override suspend fun mapToUiState(state: RootState) = state + // region Event Handling override suspend fun handleEvent(event: RootEvent) = when (event) { RootEvent.AppOpen -> handleAppOpen() } @@ -32,11 +55,15 @@ class RootViewModel @Inject constructor( delay(300) // TODO: Fix that // navigate to Onboarding navigator.navigate(Destination.onboarding.route) { - popUpTo(Destination.onboarding.route) { + popUpTo(Destination.main.route) { inclusive = true } } } } + // endregion + data class InternalState( + val baseCurrency: CurrencyCode, + ) } \ No newline at end of file diff --git a/app/src/test/java/com/ivy/wallet/ShortAmountTest.kt b/app/src/test/java/com/ivy/wallet/ShortAmountTest.kt deleted file mode 100644 index e7609a86e2..0000000000 --- a/app/src/test/java/com/ivy/wallet/ShortAmountTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.ivy.wallet - -import com.ivy.wallet.utils.hasSignificantDecimalPart -import com.ivy.wallet.utils.shortenAmount -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ShortAmountTest { - @Test - fun shorten_million() { - val amount = 1586001.23 - - val result = shortenAmount(amount) - println("result = $result") - - assertEquals("1.59m", result) - } - - @Test - fun shorten_million2() { - val amount = 1084001.23 - - val result = shortenAmount(amount) - println("result = $result") - - assertEquals("1.08m", result) - } - - @Test - fun shorten_thousands() { - val amount = 328600.23 - - val result = shortenAmount(amount) - println("result = $result") - - assertEquals("328.60k", result) - } - - @Test - fun shorten_thousands2() { - val amount = 503000.23 - - val result = shortenAmount(amount) - println("result = $result") - - assertEquals("503k", result) - } - - @Test - fun hasDecimalPart_true() { - val number = 10002341.01 - assertEquals(true, hasSignificantDecimalPart(number)) - } - - @Test - fun hasDecimalPart_false() { - val number = 10002341.00 - assertEquals(false, hasSignificantDecimalPart(number)) - } -} \ No newline at end of file diff --git a/balance-prediction/build.gradle.kts b/balance-prediction/build.gradle.kts deleted file mode 100644 index cf1ade6448..0000000000 --- a/balance-prediction/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` - `kotlin-android` -} - -dependencies { - Hilt() - - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) -} \ No newline at end of file diff --git a/balance-prediction/src/main/java/com/ivy/balance/BalanceScreen.kt b/balance-prediction/src/main/java/com/ivy/balance/BalanceScreen.kt deleted file mode 100644 index 8d326450df..0000000000 --- a/balance-prediction/src/main/java/com/ivy/balance/BalanceScreen.kt +++ /dev/null @@ -1,261 +0,0 @@ -//package com.ivy.balance -// -//import androidx.compose.foundation.layout.* -//import androidx.compose.material.Text -//import androidx.compose.runtime.* -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.draw.rotate -//import androidx.compose.ui.res.stringResource -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.compose.ui.unit.sp -//import androidx.compose.ui.zIndex -//import androidx.hilt.navigation.compose.hiltViewModel -//import com.ivy.core.ui.temp.trash.TimePeriod -//import com.ivy.design.l0_system.UI -//import com.ivy.design.l0_system.style -//import com.ivy.design.util.IvyPreview -// -// -//import com.ivy.wallet.ui.theme.Gradient -//import com.ivy.wallet.ui.theme.Gray -//import com.ivy.wallet.ui.theme.Orange -//import com.ivy.wallet.ui.theme.White -//import com.ivy.wallet.ui.theme.components.BalanceRow -//import com.ivy.wallet.ui.theme.components.IvyCircleButton -//import com.ivy.wallet.ui.theme.components.IvyDividerLine -//import com.ivy.wallet.ui.theme.modal.ChoosePeriodModal -//import com.ivy.wallet.ui.theme.modal.ChoosePeriodModalData -//import com.ivy.wallet.ui.theme.wallet.PeriodSelector -//import com.ivy.wallet.utils.format -// -//val FAB_BUTTON_SIZE = 56.dp -// -//@Composable -//fun BoxWithConstraintsScope.BalanceScreen() { -// val viewModel: BalanceViewModel = hiltViewModel() -// -// val period by viewModel.period.collectAsState() -// val baseCurrencyCode by viewModel.baseCurrencyCode.collectAsState() -// val currentBalance by viewModel.currentBalance.collectAsState() -// val plannedPaymentsAmount by viewModel.plannedPaymentsAmount.collectAsState() -// val balanceAfterPlannedPayments by viewModel.balanceAfterPlannedPayments.collectAsState() -// -// -// UI( -// period = period, -// baseCurrencyCode = baseCurrencyCode, -// currentBalance = currentBalance, -// plannedPaymentsAmount = plannedPaymentsAmount, -// balanceAfterPlannedPayments = balanceAfterPlannedPayments, -// -// onSetPeriod = viewModel::setPeriod, -// onPreviousMonth = viewModel::previousMonth, -// onNextMonth = viewModel::nextMonth -// ) -//} -// -//@Composable -//private fun BoxWithConstraintsScope.UI( -// period: TimePeriod, -// -// baseCurrencyCode: String, -// currentBalance: Double, -// plannedPaymentsAmount: Double, -// balanceAfterPlannedPayments: Double, -// -// onSetPeriod: (TimePeriod) -> Unit = {}, -// onPreviousMonth: () -> Unit = {}, -// onNextMonth: () -> Unit = {} -//) { -// var choosePeriodModal: ChoosePeriodModalData? by remember { mutableStateOf(null) } -// -// Column( -// modifier = Modifier -// .fillMaxSize() -// .statusBarsPadding() -// .navigationBarsPadding() -// ) { -// Spacer(Modifier.height(20.dp)) -// -// PeriodSelector( -// period = period, -// onPreviousMonth = onPreviousMonth, -// onNextMonth = onNextMonth, -// onShowChoosePeriodModal = { -// choosePeriodModal = ChoosePeriodModalData( -// period = period -// ) -// } -// ) -// -// Spacer(Modifier.height(32.dp)) -// -// CurrentBalance( -// currency = baseCurrencyCode, -// currentBalance = currentBalance -// ) -// -// Spacer(Modifier.height(32.dp)) -// -// IvyDividerLine( -// modifier = Modifier -// .padding(horizontal = 24.dp) -// ) -// -// Spacer(Modifier.height(40.dp)) -// -// BalanceAfterPlannedPayments( -// currency = baseCurrencyCode, -// currentBalance = currentBalance, -// plannedPaymentsAmount = plannedPaymentsAmount, -// balanceAfterPlannedPayments = balanceAfterPlannedPayments -// ) -// -// Spacer(Modifier.weight(1f)) -// -// CloseButton() -// -// Spacer(Modifier.height(48.dp)) -// } -// -// ChoosePeriodModal( -// modal = choosePeriodModal, -// dismiss = { -// choosePeriodModal = null -// } -// ) { -// onSetPeriod(it) -// } -//} -// -// -//@Composable -//private fun ColumnScope.CurrentBalance( -// currency: String, -// currentBalance: Double -//) { -// Text( -// modifier = Modifier.align(Alignment.CenterHorizontally), -// text = stringResource(R.string.current_balance), -// style = UI.typo.b2.style( -// color = Gray, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(4.dp)) -// -// BalanceRow( -// modifier = Modifier.align(Alignment.CenterHorizontally), -// currency = currency, -// balance = currentBalance -// ) -//} -// -//@Composable -//private fun ColumnScope.BalanceAfterPlannedPayments( -// currency: String, -// currentBalance: Double, -// plannedPaymentsAmount: Double, -// balanceAfterPlannedPayments: Double -//) { -// Text( -// modifier = Modifier -// .padding(horizontal = 32.dp), -// text = stringResource(R.string.balance_after_payments), -// style = UI.typo.b2.style( -// color = Orange, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Row( -// modifier = Modifier -// .fillMaxWidth(), -// verticalAlignment = Alignment.CenterVertically -// ) { -// Spacer(Modifier.width(32.dp)) -// -// BalanceRow( -// currency = currency, -// balance = balanceAfterPlannedPayments, -// -// integerFontSize = 30.sp, -// decimalFontSize = 18.sp, -// currencyFontSize = 18.sp, -// -// currencyUpfront = false -// ) -// -// Spacer(Modifier.weight(1f)) -// -// Column( -// horizontalAlignment = Alignment.End, -// ) { -// Spacer(Modifier.height(4.dp)) -// -// Text( -// text = "${currentBalance.format(2)} $currency", -// style = UI.typoSecond.c.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.Normal -// ) -// ) -// -// Spacer(Modifier.height(2.dp)) -// -// val plusSign = if (plannedPaymentsAmount >= 0) "+" else "" -// Text( -// text = "${plusSign}${plannedPaymentsAmount.format(2)} $currency", -// style = UI.typoSecond.c.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// } -// -// Spacer(Modifier.width(32.dp)) -// } -//} -// -//@Composable -//private fun ColumnScope.CloseButton() { -// -// IvyCircleButton( -// modifier = Modifier -// .align(Alignment.CenterHorizontally) -// .size(FAB_BUTTON_SIZE) -// .rotate(45f) -// .zIndex(200f), -// backgroundPadding = 8.dp, -// icon = R.drawable.ic_round_add_24, -// backgroundGradient = Gradient.solid(Gray), -// hasShadow = false, -// tint = White -// ) { -// -// } -//} -// -//@Preview -//@Composable -//private fun Preview() { -// IvyPreview { -// UI( -// period = TimePeriod.currentMonth( -// startDayOfMonth = 1 -// ), //preview -// baseCurrencyCode = "BGN", -// currentBalance = 9326.55, -// balanceAfterPlannedPayments = 8426.0, -// plannedPaymentsAmount = -900.55, -// -// onSetPeriod = {} -// ) -// } -//} diff --git a/balance-prediction/src/main/java/com/ivy/balance/BalanceViewModel.kt b/balance-prediction/src/main/java/com/ivy/balance/BalanceViewModel.kt deleted file mode 100644 index 0c44c8e362..0000000000 --- a/balance-prediction/src/main/java/com/ivy/balance/BalanceViewModel.kt +++ /dev/null @@ -1,84 +0,0 @@ -//package com.ivy.balance -// -//import androidx.lifecycle.ViewModel -//import androidx.lifecycle.viewModelScope -//import com.ivy.core.ui.temp.trash.IvyWalletCtx -//import com.ivy.core.ui.temp.trash.TimePeriod -//import com.ivy.wallet.domain.action.settings.BaseCurrencyActOld -//import com.ivy.wallet.domain.action.wallet.CalcWalletBalanceAct -//import com.ivy.wallet.utils.dateNowUTC -//import com.ivy.wallet.utils.ioThread -//import com.ivy.wallet.utils.readOnly -//import dagger.hilt.android.lifecycle.HiltViewModel -//import kotlinx.coroutines.flow.MutableStateFlow -//import kotlinx.coroutines.launch -//import javax.inject.Inject -// -//@HiltViewModel -//class BalanceViewModel @Inject constructor( -// private val plannedPaymentsLogic: PlannedPaymentsLogic, -// private val ivyContext: IvyWalletCtx, -// private val baseCurrencyAct: BaseCurrencyActOld, -// private val calcWalletBalanceAct: CalcWalletBalanceAct -//) : ViewModel() { -// -// private val _period = MutableStateFlow(ivyContext.selectedPeriod) -// val period = _period.readOnly() -// -// private val _baseCurrencyCode = MutableStateFlow("") -// val baseCurrencyCode = _baseCurrencyCode.readOnly() -// -// private val _currentBalance = MutableStateFlow(0.0) -// val currentBalance = _currentBalance.readOnly() -// -// private val _plannedPaymentsAmount = MutableStateFlow(0.0) -// val plannedPaymentsAmount = _plannedPaymentsAmount.readOnly() -// -// private val _balanceAfterPlannedPayments = MutableStateFlow(0.0) -// val balanceAfterPlannedPayments = _balanceAfterPlannedPayments.readOnly() -// -// fun start(period: TimePeriod = ivyContext.selectedPeriod) { -// viewModelScope.launch { -// _baseCurrencyCode.value = baseCurrencyAct(Unit) -// -// _period.value = period -// -// val currentBalance = calcWalletBalanceAct( -// CalcWalletBalanceAct.Input(baseCurrencyCode.value) -// ).toDouble() -// -// _currentBalance.value = currentBalance -// -// val plannedPaymentsAmount = ioThread { -// plannedPaymentsLogic.plannedPaymentsAmountFor(period.toRange(ivyContext.startDayOfMonth)) //+ positive if Income > Expenses else - negative -// } -// _plannedPaymentsAmount.value = plannedPaymentsAmount -// -// _balanceAfterPlannedPayments.value = currentBalance + plannedPaymentsAmount -// } -// } -// -// fun setPeriod(period: TimePeriod) { -// start(period = period) -// } -// -// fun nextMonth() { -// val month = period.value.month -// val year = period.value.year ?: dateNowUTC().year -// if (month != null) { -// start( -// period = month.incrementMonthPeriod(ivyContext, 1L, year = year), -// ) -// } -// } -// -// fun previousMonth() { -// val month = period.value.month -// val year = period.value.year ?: dateNowUTC().year -// if (month != null) { -// start( -// period = month.incrementMonthPeriod(ivyContext, -1L, year = year), -// ) -// } -// } -//} \ No newline at end of file diff --git a/billing/build.gradle.kts b/billing/build.gradle.kts index 2464262f32..ef5af5b050 100644 --- a/billing/build.gradle.kts +++ b/billing/build.gradle.kts @@ -13,7 +13,6 @@ dependencies { implementation(project(":common:main")) implementation(project(":core:data-model")) - implementation(project(":app-base")) implementation(project(":core:ui")) Billing(api = true) diff --git a/billing/src/main/java/com/ivy/billing/IvyBilling.kt b/billing/src/main/java/com/ivy/billing/IvyBilling.kt index 4191e97efd..813ffb3264 100644 --- a/billing/src/main/java/com/ivy/billing/IvyBilling.kt +++ b/billing/src/main/java/com/ivy/billing/IvyBilling.kt @@ -1,9 +1,6 @@ package com.ivy.billing -import android.app.Activity -import com.android.billingclient.api.* -import com.ivy.wallet.utils.ioThread -import timber.log.Timber +import com.android.billingclient.api.BillingClient class IvyBilling( @@ -41,166 +38,166 @@ class IvyBilling( } private lateinit var billingClient: BillingClient - - fun init( - activity: Activity, - onReady: () -> Unit, - onPurchases: (List) -> Unit, - onError: (code: Int, msg: String) -> Unit, - ) { - val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { - onPurchases(purchases) - } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { - onError(billingResult.responseCode, billingResult.debugMessage) - } else { - onError(billingResult.responseCode, billingResult.debugMessage) - } - - } - - billingClient = BillingClient.newBuilder(activity) - .setListener(purchasesUpdatedListener) - .enablePendingPurchases() - .build() - - billingClient.startConnection(object : BillingClientStateListener { - override fun onBillingSetupFinished(billingResult: BillingResult) { - if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { - // The BillingClient is ready. You can query purchases here. - onReady() - } else { - onError(billingResult.responseCode, billingResult.debugMessage) - } - } - - override fun onBillingServiceDisconnected() { - // Try to restart the connection on the next request to - // Google Play by calling the startConnection() method. - onError(-666, "onBillingServiceDisconnected") - } - }) - } - - suspend fun queryPurchases(): List { - return ioThread { - try { - queryBoughtSubscriptions() - .plus(queryBoughtOneTimeOffers()) - } catch (e: Exception) { - e.printStackTrace() - emptyList() - } - } - } - - private suspend fun queryBoughtSubscriptions(): List { - return billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS).purchasesList - } - - private suspend fun queryBoughtOneTimeOffers(): List { - return try { - billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP).purchasesList - } catch (e: Exception) { - e.printStackTrace() - emptyList() - } - } - - suspend fun fetchPlans(): List { - return fetchSubscriptions().plus(fetchOneTimePlans()) - } - - private suspend fun fetchSubscriptions(): List { - val params = SkuDetailsParams.newBuilder() - .setSkusList(SUBSCRIPTIONS) - .setType(BillingClient.SkuType.SUBS) - - // leverage querySkuDetails Kotlin extension function - val skuDetailsResult = ioThread { - billingClient.querySkuDetails(params.build()) - } - - return skuDetailsResult.skuDetailsList - .orEmpty() - .map { - val type = when (it.subscriptionPeriod) { - "P1M" -> PlanType.MONTHLY - "P6M" -> PlanType.SIX_MONTH - "P1Y" -> PlanType.YEARLY - else -> return@map null - } - Plan( - sku = it.sku, - type = type, - price = it.price, - skuDetails = it - ) - } - .filterNotNull() - } - - suspend fun fetchOneTimePlans(): List { - val params = SkuDetailsParams.newBuilder() - .setSkusList(ONE_TIME_PLANS) - .setType(BillingClient.SkuType.INAPP) - - // leverage querySkuDetails Kotlin extension function - val skuDetailsResult = ioThread { - billingClient.querySkuDetails(params.build()) - } - - return skuDetailsResult.skuDetailsList - .orEmpty() - .map { - Plan( - sku = it.sku, - type = PlanType.LIFETIME, - price = it.price, - skuDetails = it - ) - } - } - - fun buy( - activity: Activity, - skuToBuy: SkuDetails, - oldSubscriptionPurchaseToken: String? - ) { - val flowBuilder = BillingFlowParams.newBuilder() - .setSkuDetails(skuToBuy) - - if (oldSubscriptionPurchaseToken != null && oldSubscriptionPurchaseToken.isNotBlank()) { - flowBuilder.setSubscriptionUpdateParams( - BillingFlowParams.SubscriptionUpdateParams - .newBuilder() - .setOldSkuPurchaseToken(oldSubscriptionPurchaseToken) - .build() - ) - } - - val billingResult = billingClient.launchBillingFlow(activity, flowBuilder.build()) - Timber.i("buy(): code=${billingResult.responseCode}, msg: ${billingResult.debugMessage}") - } - - suspend fun checkPremium( - purchase: Purchase, - onActivatePremium: (Purchase) -> Unit - ) { - if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { - // Grant entitlement to the user. - onActivatePremium(purchase) - - if (!purchase.isAcknowledged) { - val acknowledgeResult = ioThread { - billingClient.acknowledgePurchase( - AcknowledgePurchaseParams.newBuilder() - .setPurchaseToken(purchase.purchaseToken) - .build() - ) - } - Timber.i("Acknowledge purchase result, code=${acknowledgeResult.responseCode}: ${acknowledgeResult.debugMessage}") - } - } - } +// +// fun init( +// activity: Activity, +// onReady: () -> Unit, +// onPurchases: (List) -> Unit, +// onError: (code: Int, msg: String) -> Unit, +// ) { +// val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> +// if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { +// onPurchases(purchases) +// } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { +// onError(billingResult.responseCode, billingResult.debugMessage) +// } else { +// onError(billingResult.responseCode, billingResult.debugMessage) +// } +// +// } +// +// billingClient = BillingClient.newBuilder(activity) +// .setListener(purchasesUpdatedListener) +// .enablePendingPurchases() +// .build() +// +// billingClient.startConnection(object : BillingClientStateListener { +// override fun onBillingSetupFinished(billingResult: BillingResult) { +// if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { +// // The BillingClient is ready. You can query purchases here. +// onReady() +// } else { +// onError(billingResult.responseCode, billingResult.debugMessage) +// } +// } +// +// override fun onBillingServiceDisconnected() { +// // Try to restart the connection on the next request to +// // Google Play by calling the startConnection() method. +// onError(-666, "onBillingServiceDisconnected") +// } +// }) +// } +// +// suspend fun queryPurchases(): List { +// return ioThread { +// try { +// queryBoughtSubscriptions() +// .plus(queryBoughtOneTimeOffers()) +// } catch (e: Exception) { +// e.printStackTrace() +// emptyList() +// } +// } +// } +// +// private suspend fun queryBoughtSubscriptions(): List { +// return billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS).purchasesList +// } +// +// private suspend fun queryBoughtOneTimeOffers(): List { +// return try { +// billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP).purchasesList +// } catch (e: Exception) { +// e.printStackTrace() +// emptyList() +// } +// } +// +// suspend fun fetchPlans(): List { +// return fetchSubscriptions().plus(fetchOneTimePlans()) +// } +// +// private suspend fun fetchSubscriptions(): List { +// val params = SkuDetailsParams.newBuilder() +// .setSkusList(SUBSCRIPTIONS) +// .setType(BillingClient.SkuType.SUBS) +// +// // leverage querySkuDetails Kotlin extension function +// val skuDetailsResult = ioThread { +// billingClient.querySkuDetails(params.build()) +// } +// +// return skuDetailsResult.skuDetailsList +// .orEmpty() +// .map { +// val type = when (it.subscriptionPeriod) { +// "P1M" -> PlanType.MONTHLY +// "P6M" -> PlanType.SIX_MONTH +// "P1Y" -> PlanType.YEARLY +// else -> return@map null +// } +// Plan( +// sku = it.sku, +// type = type, +// price = it.price, +// skuDetails = it +// ) +// } +// .filterNotNull() +// } +// +// suspend fun fetchOneTimePlans(): List { +// val params = SkuDetailsParams.newBuilder() +// .setSkusList(ONE_TIME_PLANS) +// .setType(BillingClient.SkuType.INAPP) +// +// // leverage querySkuDetails Kotlin extension function +// val skuDetailsResult = ioThread { +// billingClient.querySkuDetails(params.build()) +// } +// +// return skuDetailsResult.skuDetailsList +// .orEmpty() +// .map { +// Plan( +// sku = it.sku, +// type = PlanType.LIFETIME, +// price = it.price, +// skuDetails = it +// ) +// } +// } +// +// fun buy( +// activity: Activity, +// skuToBuy: SkuDetails, +// oldSubscriptionPurchaseToken: String? +// ) { +// val flowBuilder = BillingFlowParams.newBuilder() +// .setSkuDetails(skuToBuy) +// +// if (oldSubscriptionPurchaseToken != null && oldSubscriptionPurchaseToken.isNotBlank()) { +// flowBuilder.setSubscriptionUpdateParams( +// BillingFlowParams.SubscriptionUpdateParams +// .newBuilder() +// .setOldSkuPurchaseToken(oldSubscriptionPurchaseToken) +// .build() +// ) +// } +// +// val billingResult = billingClient.launchBillingFlow(activity, flowBuilder.build()) +// Timber.i("buy(): code=${billingResult.responseCode}, msg: ${billingResult.debugMessage}") +// } +// +// suspend fun checkPremium( +// purchase: Purchase, +// onActivatePremium: (Purchase) -> Unit +// ) { +// if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { +// // Grant entitlement to the user. +// onActivatePremium(purchase) +// +// if (!purchase.isAcknowledged) { +// val acknowledgeResult = ioThread { +// billingClient.acknowledgePurchase( +// AcknowledgePurchaseParams.newBuilder() +// .setPurchaseToken(purchase.purchaseToken) +// .build() +// ) +// } +// Timber.i("Acknowledge purchase result, code=${acknowledgeResult.responseCode}: ${acknowledgeResult.debugMessage}") +// } +// } +// } } \ No newline at end of file diff --git a/budgets/build.gradle.kts b/budgets/build.gradle.kts deleted file mode 100644 index 11ef563d20..0000000000 --- a/budgets/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) -} \ No newline at end of file diff --git a/budgets/src/main/AndroidManifest.xml b/budgets/src/main/AndroidManifest.xml deleted file mode 100644 index f6a1871b0d..0000000000 --- a/budgets/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/budgets/src/main/java/com/ivy/budgets/BudgetBottomBar.kt b/budgets/src/main/java/com/ivy/budgets/BudgetBottomBar.kt deleted file mode 100644 index 53d4ca21f3..0000000000 --- a/budgets/src/main/java/com/ivy/budgets/BudgetBottomBar.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.ivy.budgets - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Blue -import com.ivy.wallet.ui.theme.components.BackBottomBar -import com.ivy.wallet.ui.theme.components.IvyButton - -@Composable -internal fun BoxWithConstraintsScope.BudgetBottomBar( - onClose: () -> Unit, - onAdd: () -> Unit -) { - BackBottomBar(onBack = onClose) { - IvyButton( - text = stringResource(R.string.add_budget), - iconStart = R.drawable.ic_plus - ) { - onAdd() - } - } -} - -@Preview -@Composable -private fun PreviewBottomBar() { - IvyPreview { - Column( - Modifier - .fillMaxSize() - .background(Blue) - ) { - - } - - BudgetBottomBar( - onAdd = {}, - onClose = {} - ) - } -} diff --git a/budgets/src/main/java/com/ivy/budgets/BudgetScreen.kt b/budgets/src/main/java/com/ivy/budgets/BudgetScreen.kt deleted file mode 100644 index 8db2d9b586..0000000000 --- a/budgets/src/main/java/com/ivy/budgets/BudgetScreen.kt +++ /dev/null @@ -1,446 +0,0 @@ -//package com.ivy.budgets -// -//import androidx.compose.foundation.layout.* -//import androidx.compose.foundation.rememberScrollState -//import androidx.compose.foundation.verticalScroll -//import androidx.compose.material.Text -//import androidx.compose.runtime.* -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.platform.testTag -//import androidx.compose.ui.res.stringResource -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.text.style.TextAlign -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.hilt.navigation.compose.hiltViewModel -//import com.ivy.base.FromToTimeRange -//import com.ivy.budgets.model.DisplayBudget -//import com.ivy.core.ui.temp.trash.BudgetExt -//import com.ivy.core.ui.temp.trash.TimePeriod -//import com.ivy.core.ui.temp.trash.parseCategoryIds -//import com.ivy.data.AccountOld -//import com.ivy.data.Budget -//import com.ivy.data.CategoryOld -//import com.ivy.design.l0_system.UI -//import com.ivy.design.l0_system.style -//import com.ivy.design.util.IvyPreview -// -// -//import com.ivy.wallet.domain.deprecated.logic.model.CreateBudgetData -//import com.ivy.wallet.ui.theme.Gray -//import com.ivy.wallet.ui.theme.components.BudgetBattery -//import com.ivy.wallet.ui.theme.components.IvyIcon -//import com.ivy.wallet.ui.theme.components.ReorderButton -//import com.ivy.wallet.ui.theme.components.ReorderModalSingleType -//import com.ivy.wallet.ui.theme.modal.BudgetModal -//import com.ivy.wallet.ui.theme.modal.BudgetModalData -//import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 -//import com.ivy.wallet.utils.clickableNoIndication -//import com.ivy.wallet.utils.format -// -//@Composable -//fun BoxWithConstraintsScope.BudgetScreen() { -// val viewModel: BudgetViewModel = hiltViewModel() -// -// val timeRange by viewModel.timeRange.collectAsState() -// val baseCurrency by viewModel.baseCurrencyCode.collectAsState() -// val categories by viewModel.categories.collectAsState() -// val accounts by viewModel.accounts.collectAsState() -// val budgets by viewModel.budgets.collectAsState() -// val appBudgetMax by viewModel.appBudgetMax.collectAsState() -// val categoryBudgetsTotal by viewModel.categoryBudgetsTotal.collectAsState() -// -// UI( -// timeRange = timeRange, -// baseCurrency = baseCurrency, -// categories = categories, -// accounts = accounts, -// displayBudgets = budgets, -// appBudgetMax = appBudgetMax, -// categoryBudgetsTotal = categoryBudgetsTotal, -// -// onCreateBudget = viewModel::createBudget, -// onEditBudget = viewModel::editBudget, -// onDeleteBudget = viewModel::deleteBudget, -// onReorder = viewModel::reorder -// ) -//} -// -//@Composable -//private fun BoxWithConstraintsScope.UI( -// timeRange: FromToTimeRange?, -// baseCurrency: String, -// categories: List, -// accounts: List, -// displayBudgets: List, -// appBudgetMax: Double, -// categoryBudgetsTotal: Double, -// -// onCreateBudget: (CreateBudgetData) -> Unit = {}, -// onEditBudget: (Budget) -> Unit = {}, -// onDeleteBudget: (Budget) -> Unit = {}, -// onReorder: (List) -> Unit = {} -//) { -// var reorderModalVisible by remember { mutableStateOf(false) } -// var budgetModalData: BudgetModalData? by remember { mutableStateOf(null) } -// -// Column( -// modifier = Modifier -// .fillMaxSize() -// .systemBarsPadding() -// .verticalScroll(rememberScrollState()), -// ) { -// Spacer(Modifier.height(32.dp)) -// -// Toolbar( -// timeRange = timeRange, -// baseCurrency = baseCurrency, -// appBudgetMax = appBudgetMax, -// categoryBudgetsTotal = categoryBudgetsTotal, -// setReorderModalVisible = { -// reorderModalVisible = it -// } -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// for (item in displayBudgets) { -// Spacer(Modifier.height(24.dp)) -// -// BudgetItem( -// displayBudget = item, -// baseCurrency = baseCurrency -// ) { -// budgetModalData = BudgetModalData( -// budget = item.budget, -// baseCurrency = baseCurrency, -// categories = categories, -// accounts = accounts, -// autoFocusKeyboard = false -// ) -// } -// } -// -// if (displayBudgets.isEmpty()) { -// Spacer(Modifier.weight(1f)) -// -// NoBudgetsEmptyState( -// emptyStateTitle = stringResource(R.string.no_budgets), -// emptyStateText = stringResource(R.string.no_budgets_text) -// ) -// -// Spacer(Modifier.weight(1f)) -// } -// -// Spacer(Modifier.height(150.dp)) //scroll hack -// } -// -// -// BudgetBottomBar( -// onAdd = { -// budgetModalData = BudgetModalData( -// budget = null, -// baseCurrency = baseCurrency, -// categories = categories, -// accounts = accounts -// ) -// }, -// onClose = { -// -// }, -// ) -// -// ReorderModalSingleType( -// visible = reorderModalVisible, -// initialItems = displayBudgets, -// dismiss = { -// reorderModalVisible = false -// }, -// onReordered = onReorder -// ) { _, item -> -// Text( -// modifier = Modifier -// .fillMaxWidth() -// .padding(end = 24.dp) -// .padding(vertical = 8.dp), -// text = item.budget.name, -// style = UI.typo.b1.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.Bold -// ) -// ) -// } -// -// BudgetModal( -// modal = budgetModalData, -// onCreate = onCreateBudget, -// onEdit = onEditBudget, -// onDelete = onDeleteBudget, -// dismiss = { -// budgetModalData = null -// } -// ) -//} -// -//@Composable -//private fun Toolbar( -// timeRange: FromToTimeRange?, -// baseCurrency: String, -// appBudgetMax: Double, -// categoryBudgetsTotal: Double, -// -// setReorderModalVisible: (Boolean) -> Unit -//) { -// Row( -// modifier = Modifier.fillMaxWidth(), -// verticalAlignment = Alignment.CenterVertically -// ) { -// Column( -// modifier = Modifier -// .weight(1f) -// .padding(start = 24.dp, end = 16.dp) -// ) { -// Text( -// text = stringResource(R.string.budgets), -// style = UI.typo.h2.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// if (timeRange != null) { -// Spacer(Modifier.height(4.dp)) -// -// Text( -// text = timeRange.toDisplay(), -// style = UI.typo.b2.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.Medium -// ) -// ) -// } -// -// if (categoryBudgetsTotal > 0 || appBudgetMax > 0) { -// Spacer(Modifier.height(4.dp)) -// -// val categoryBudgetText = if (categoryBudgetsTotal > 0) { -// stringResource( -// R.string.for_categories, -// categoryBudgetsTotal.format(baseCurrency), -// baseCurrency -// ) -// } else "" -// -// val appBudgetMaxText = if (appBudgetMax > 0) { -// stringResource( -// R.string.app_budget, -// appBudgetMax.format(baseCurrency), -// baseCurrency -// ) -// } else "" -// -// val hasBothBudgetTypes = -// categoryBudgetText.isNotBlank() && appBudgetMaxText.isNotBlank() -// Text( -// modifier = Modifier.testTag("budgets_info_text"), -// text = if (hasBothBudgetTypes) -// stringResource( -// R.string.budget_info_both, -// categoryBudgetText, -// appBudgetMaxText -// ) -// else stringResource(R.string.budget_info, categoryBudgetText, appBudgetMaxText), -// style = UI.typoSecond.c.style( -// color = Gray, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// } -// -// } -// -// ReorderButton { -// setReorderModalVisible(true) -// } -// -// Spacer(Modifier.width(24.dp)) -// } -//} -// -//@Composable -//private fun BudgetItem( -// displayBudget: DisplayBudget, -// baseCurrency: String, -// -// onClick: () -> Unit -//) { -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .clickableNoIndication { -// onClick() -// }, -// verticalAlignment = Alignment.CenterVertically -// ) { -// Column( -// modifier = Modifier -// .weight(1f) -// .padding(horizontal = 24.dp) -// ) { -// Text( -// text = displayBudget.budget.name, -// style = UI.typo.b1.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(2.dp)) -// -// Text( -// text = BudgetExt.type(displayBudget.budget.parseCategoryIds().size), -// style = UI.typo.c.style( -// color = Gray -// ) -// ) -// } -// -// AmountCurrencyB1( -// amount = displayBudget.budget.amount, -// currency = baseCurrency, -// amountFontWeight = FontWeight.ExtraBold -// ) -// -// Spacer(Modifier.width(32.dp)) -// } -// -// Spacer(Modifier.height(12.dp)) -// -// BudgetBattery( -// modifier = Modifier.padding(horizontal = 16.dp), -// currency = baseCurrency, -// expenses = displayBudget.spentAmount, -// budget = displayBudget.budget.amount, -// backgroundNotFilled = UI.colors.medium -// ) { -// onClick() -// } -//} -// -//@Composable -//private fun NoBudgetsEmptyState( -// modifier: Modifier = Modifier, -// emptyStateTitle: String, -// emptyStateText: String, -//) { -// Column( -// modifier = modifier.fillMaxWidth(), -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Spacer(Modifier.height(32.dp)) -// -// IvyIcon( -// icon = R.drawable.ic_budget_xl, -// tint = Gray -// ) -// -// Spacer(Modifier.height(24.dp)) -// -// Text( -// text = emptyStateTitle, -// style = UI.typo.b1.style( -// color = Gray, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Text( -// modifier = Modifier.padding(horizontal = 32.dp), -// text = emptyStateText, -// style = UI.typo.b2.style( -// color = Gray, -// fontWeight = FontWeight.Medium, -// textAlign = TextAlign.Center -// ) -// ) -// -// Spacer(Modifier.height(96.dp)) -// } -//} -// -// -//@Preview -//@Composable -//private fun Preview_Empty() { -// IvyPreview { -// UI( -// timeRange = TimePeriod.currentMonth( -// startDayOfMonth = 1 -// ).toRange(1), //preview -// baseCurrency = "BGN", -// categories = emptyList(), -// accounts = emptyList(), -// displayBudgets = emptyList(), -// appBudgetMax = 5000.0, -// categoryBudgetsTotal = 2400.0, -// -// onReorder = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun Preview_Budgets() { -// IvyPreview { -// UI( -// timeRange = TimePeriod.currentMonth( -// startDayOfMonth = 1 -// ).toRange(1), //preview -// baseCurrency = "BGN", -// categories = emptyList(), -// accounts = emptyList(), -// appBudgetMax = 5000.0, -// categoryBudgetsTotal = 0.0, -// displayBudgets = listOf( -// DisplayBudget( -// budget = Budget( -// name = "Ivy Marketing", -// amount = 1000.0, -// accountIdsSerialized = null, -// categoryIdsSerialized = null, -// orderId = 0.0 -// ), -// spentAmount = 260.0 -// ), -// -// DisplayBudget( -// budget = Budget( -// name = "Ivy Marketing 2", -// amount = 1000.0, -// accountIdsSerialized = null, -// categoryIdsSerialized = null, -// orderId = 0.0 -// ), -// spentAmount = 351.0 -// ), -// -// DisplayBudget( -// budget = Budget( -// name = "Baldr Products, Fidgets", -// amount = 750.0, -// accountIdsSerialized = null, -// categoryIdsSerialized = "cat1,cat2,cat3", -// orderId = 0.1 -// ), -// spentAmount = 50.0 -// ), -// ), -// -// onReorder = {} -// ) -// } -//} diff --git a/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt b/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt deleted file mode 100644 index cce206b59a..0000000000 --- a/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt +++ /dev/null @@ -1,222 +0,0 @@ -//package com.ivy.budgets -// -//import androidx.lifecycle.ViewModel -//import androidx.lifecycle.viewModelScope -//import com.ivy.base.toCloseTimeRange -//import com.ivy.budgets.model.DisplayBudget -//import com.ivy.core.ui.temp.trash.IvyWalletCtx -//import com.ivy.core.ui.temp.trash.TimePeriod -//import com.ivy.core.ui.temp.trash.parseAccountIds -//import com.ivy.core.ui.temp.trash.parseCategoryIds -//import com.ivy.data.AccountOld -//import com.ivy.data.Budget -//import com.ivy.data.CategoryOld -//import com.ivy.data.getDefaultFIATCurrency -//import com.ivy.data.transaction.TransactionOld -//import com.ivy.data.transaction.TrnTypeOld -//import com.ivy.frp.sumOfSuspend -//import com.ivy.frp.test.TestIdlingResource -//import com.ivy.temp.persistence.ExchangeActOld -//import com.ivy.temp.persistence.ExchangeData -//import com.ivy.wallet.domain.action.account.AccountsActOld -//import com.ivy.wallet.domain.action.budget.BudgetsAct -//import com.ivy.wallet.domain.action.category.CategoriesActOld -//import com.ivy.wallet.domain.action.global.StartDayOfMonthAct -//import com.ivy.wallet.domain.action.settings.BaseCurrencyActOld -//import com.ivy.wallet.domain.action.transaction.HistoryTrnsAct -//import com.ivy.wallet.domain.deprecated.logic.model.CreateBudgetData -//import com.ivy.wallet.domain.deprecated.sync.item.BudgetSync -//import com.ivy.wallet.domain.pure.transaction.trnCurrency -//import com.ivy.wallet.io.persistence.dao.BudgetDao -//import com.ivy.wallet.io.persistence.data.toEntity -//import com.ivy.wallet.utils.ioThread -//import com.ivy.wallet.utils.isNotNullOrBlank -//import com.ivy.wallet.utils.readOnly -//import dagger.hilt.android.lifecycle.HiltViewModel -//import kotlinx.coroutines.flow.MutableStateFlow -//import kotlinx.coroutines.launch -//import javax.inject.Inject -// -//@HiltViewModel -//class BudgetViewModel @Inject constructor( -// private val budgetDao: BudgetDao, -// private val budgetCreator: BudgetCreator, -// private val budgetSync: BudgetSync, -// private val ivyContext: IvyWalletCtx, -// private val accountsAct: AccountsActOld, -// private val categoriesAct: CategoriesActOld, -// private val budgetsAct: BudgetsAct, -// private val baseCurrencyAct: BaseCurrencyActOld, -// private val historyTrnsAct: HistoryTrnsAct, -// private val exchangeAct: ExchangeActOld, -// private val startDayOfMonthAct: StartDayOfMonthAct, -//) : ViewModel() { -// -// private val _timeRange = MutableStateFlow(ivyContext.selectedPeriod.toRange(1)) -// val timeRange = _timeRange.readOnly() -// -// private val _baseCurrencyCode = MutableStateFlow(getDefaultFIATCurrency().currencyCode) -// val baseCurrencyCode = _baseCurrencyCode.readOnly() -// -// private val _budgets = MutableStateFlow>(emptyList()) -// val budgets = _budgets.readOnly() -// -// private val _categories = MutableStateFlow>(emptyList()) -// val categories = _categories.readOnly() -// -// private val _accounts = MutableStateFlow>(emptyList()) -// val accounts = _accounts.readOnly() -// -// private val _categoryBudgetsTotal = MutableStateFlow(0.0) -// val categoryBudgetsTotal = _categoryBudgetsTotal.readOnly() -// -// private val _appBudgetMax = MutableStateFlow(0.0) -// val appBudgetMax = _appBudgetMax.readOnly() -// -// fun start() { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// _categories.value = categoriesAct(Unit) -// -// val accounts = accountsAct(Unit) -// _accounts.value = accounts -// -// val baseCurrency = baseCurrencyAct(Unit) -// _baseCurrencyCode.value = baseCurrency -// -// val startDateOfMonth = startDayOfMonthAct(Unit) -// val timeRange = TimePeriod.currentMonth( -// startDayOfMonth = startDateOfMonth -// ).toRange(startDateOfMonth = startDateOfMonth) -// _timeRange.value = timeRange -// -// val budgets = budgetsAct(Unit) -// -// _appBudgetMax.value = budgets -// .filter { it.categoryIdsSerialized.isNullOrBlank() } -// .maxOfOrNull { it.amount } ?: 0.0 -// -// _categoryBudgetsTotal.value = budgets -// .filter { it.categoryIdsSerialized.isNotNullOrBlank() } -// .sumOf { it.amount } -// -// _budgets.value = ioThread { -// budgets.map { -// DisplayBudget( -// budget = it, -// spentAmount = calculateSpentAmount( -// budget = it, -// transactions = historyTrnsAct(timeRange.toCloseTimeRange()), -// accounts = accounts, -// baseCurrencyCode = baseCurrency -// ) -// ) -// } -// }!! -// -// TestIdlingResource.decrement() -// } -// } -// -// private suspend fun calculateSpentAmount( -// budget: Budget, -// transactions: List, -// baseCurrencyCode: String, -// accounts: List -// ): Double { -// //TODO: Re-work this by creating an FPAction for it -// val accountsFilter = budget.parseAccountIds() -// val categoryFilter = budget.parseCategoryIds() -// -// return transactions -// .filter { accountsFilter.isEmpty() || accountsFilter.contains(it.accountId) } -// .filter { categoryFilter.isEmpty() || categoryFilter.contains(it.categoryId) } -// .sumOfSuspend { -// when (it.type) { -// TrnTypeOld.INCOME -> { -// //decrement spent amount if it's not global budget -// 0.0 //ignore income -//// if (categoryFilter.isEmpty()) 0.0 else -amountBaseCurrency -// } -// TrnTypeOld.EXPENSE -> { -// //increment spent amount -// exchangeAct( -// ExchangeActOld.Input( -// data = ExchangeData( -// baseCurrency = baseCurrencyCode, -// fromCurrency = trnCurrency(it, accounts, baseCurrencyCode) -// ), -// amount = it.amount -// ) -// ).orNull()?.toDouble() ?: 0.0 -// } -// TrnTypeOld.TRANSFER -> { -// //ignore transfers for simplicity -// 0.0 -// } -// } -// } -// } -// -// fun createBudget(data: CreateBudgetData) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// budgetCreator.createBudget(data) { -// start() -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun editBudget(budget: Budget) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// budgetCreator.editBudget(budget) { -// start() -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun deleteBudget(budget: Budget) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// budgetCreator.deleteBudget(budget) { -// start() -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// -// fun reorder(newOrder: List) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// ioThread { -// newOrder.forEachIndexed { index, item -> -// budgetDao.save( -// item.budget.toEntity().copy( -// orderId = index.toDouble(), -// isSynced = false -// ) -// ) -// } -// } -// start() -// -// ioThread { -// budgetSync.sync() -// } -// -// TestIdlingResource.decrement() -// } -// } -//} \ No newline at end of file diff --git a/budgets/src/main/java/com/ivy/budgets/model/DisplayBudget.kt b/budgets/src/main/java/com/ivy/budgets/model/DisplayBudget.kt deleted file mode 100644 index a7b81c662c..0000000000 --- a/budgets/src/main/java/com/ivy/budgets/model/DisplayBudget.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.ivy.budgets.model - -import com.ivy.base.Reorderable -import com.ivy.data.Budget - -data class DisplayBudget( - val budget: Budget, - val spentAmount: Double -) : Reorderable { - override fun getItemOrderNum(): Double { - return budget.orderId - } - - override fun withNewOrderNum(newOrderNum: Double): Reorderable { - return this.copy( - budget = budget.copy( - orderId = newOrderNum - ) - ) - } -} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 644b5525c8..a03049ed9b 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -10,17 +10,17 @@ repositories { dependencies { //https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google - implementation("com.android.tools.build:gradle:7.4.0-beta01") + implementation("com.android.tools.build:gradle:7.4.0-rc01") //https://kotlinlang.org/docs/releases.html#release-details // Must match kotlinVersion from dependencies.kt - val kotlinVersion = "1.7.10" + val kotlinVersion = "1.7.20" implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") implementation(kotlin("serialization", version = kotlinVersion)) //https://developer.android.com/training/dependency-injection/hilt-android // Must match hiltVersion from dependencies.kt - implementation("com.google.dagger:hilt-android-gradle-plugin:2.42") + implementation("com.google.dagger:hilt-android-gradle-plugin:2.44") //URL: https://developers.google.com/android/guides/google-services-plugin implementation("com.google.gms:google-services:4.3.13") diff --git a/buildSrc/src/main/java/com/ivy/buildsrc/dependencies.kt b/buildSrc/src/main/java/com/ivy/buildsrc/dependencies.kt index df1546b6af..480c22d45f 100644 --- a/buildSrc/src/main/java/com/ivy/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/com/ivy/buildsrc/dependencies.kt @@ -36,37 +36,43 @@ object Project { object Versions { //https://kotlinlang.org/docs/releases.html#release-details - //WARNING: Version is also updated from buildSrc - const val kotlin = "1.7.10" - const val coroutines = "1.6.3" + //WARNING: Version must match in buildSrc build.gradle.kts + const val kotlin = "1.7.20" + //https://github.com/Kotlin/kotlinx.coroutines + const val coroutines = "1.6.4" + // region Compose //https://developer.android.com/jetpack/androidx/releases/compose - const val compose = "1.3.0-beta03" + const val compose = "1.3.2" + + //https://developer.android.com/jetpack/androidx/releases/compose-material + const val composeMaterial = "1.3.1" //https://developer.android.com/jetpack/androidx/releases/compose-compiler - const val composeCompilerVersion = "1.3.1" + const val composeCompilerVersion = "1.3.2" //https://developer.android.com/jetpack/androidx/releases/compose-foundation - const val composeFoundation = "1.2.0-rc03" + const val composeFoundation = "1.3.1" //https://developer.android.com/jetpack/compose/navigation const val navigationCompose = "2.5.1" //https://developer.android.com/jetpack/androidx/releases/activity - const val composeActivity = "1.5.0" + const val composeActivity = "1.6.1" //https://developer.android.com/jetpack/androidx/releases/lifecycle - const val composeViewModel = "2.6.0-alpha01" + const val composeViewModel = "2.6.0-alpha03" //https://developer.android.com/jetpack/androidx/releases/glance - const val composeGlance = "1.0.0-alpha03" + const val composeGlance = "1.0.0-alpha05" //Set status bar color //https://google.github.io/accompanist/systemuicontroller/ - const val composeAccompanistUIController = "0.24.13-rc" + const val composeAccompanistUIController = "0.28.0" //https://coil-kt.github.io/coil/compose/ - const val composeCoil = "2.1.0" + const val composeCoil = "2.2.2" + // endregion //https://arrow-kt.io/docs/quickstart/ const val arrow: String = "1.0.1" @@ -77,13 +83,13 @@ object Versions { //https://developer.android.com/training/dependency-injection/hilt-android //WARNING: Update hilt gradle plugin from buildSrc - const val hilt = "2.42" + const val hilt = "2.44" //https://mvnrepository.com/artifact/androidx.hilt/hilt-compiler?repo=google const val hiltX = "1.0.0" //https://developer.android.com/jetpack/androidx/releases/hilt - const val hiltNavigationCompose = "1.0.0" + const val hiltNavigationCompose = "1.1.0-alpha01" //https://developer.android.com/jetpack/androidx/releases/appcompat const val appCompat = "1.4.2" @@ -157,7 +163,7 @@ object Versions { } fun DependencyHandler.DataStore(api: Boolean) { - dependency("androidx.datastore:datastore-preferences:1.0.0", api = api) + dependency("androidx.datastore:datastore-preferences:${Versions.dataStore}", api = api) } /** @@ -171,9 +177,9 @@ fun DependencyHandler.Kotlin(api: Boolean) { } fun DependencyHandler.Compose(api: Boolean) { - val version = Versions.compose + val composeVersion = Versions.compose //URL: https://developer.android.com/jetpack/androidx/releases/compose - dependency("androidx.compose.ui:ui:$version", api = api) + dependency("androidx.compose.ui:ui:$composeVersion", api = api) dependency( "androidx.compose.foundation:foundation:${Versions.composeFoundation}", api = api @@ -182,12 +188,14 @@ fun DependencyHandler.Compose(api: Boolean) { "androidx.compose.foundation:foundation-layout:${Versions.composeFoundation}", api = api ) - dependency("androidx.compose.animation:animation:$version", api = api) - dependency("androidx.compose.material:material:$version", api = api) - dependency("androidx.compose.material:material-icons-extended:$version", api = api) - dependency("androidx.compose.runtime:runtime-livedata:$version", api = api) - debugDependency("androidx.compose.ui:ui-tooling:$version", api = api) - dependency("androidx.compose.ui:ui-tooling-preview:$version", api = api) + dependency("androidx.compose.animation:animation:$composeVersion", api = api) + dependency("androidx.compose.material:material:${Versions.composeMaterial}", api = api) + dependency( + "androidx.compose.material:material-icons-extended:${Versions.composeMaterial}", api = api + ) + dependency("androidx.compose.runtime:runtime-livedata:$composeVersion", api = api) + debugDependency("androidx.compose.ui:ui-tooling:$composeVersion", api = api) + dependency("androidx.compose.ui:ui-tooling-preview:$composeVersion", api = api) dependency( "androidx.navigation:navigation-compose:${Versions.navigationCompose}", api = api @@ -202,9 +210,6 @@ fun DependencyHandler.Compose(api: Boolean) { api = api ) - // Jetpack Glance (Compose Widgets) - dependency("androidx.glance:glance-appwidget:${Versions.composeGlance}", api = api) - Accompanist(api = api) Coil(api = api) @@ -212,6 +217,11 @@ fun DependencyHandler.Compose(api: Boolean) { ComposeTesting(api = api) } +fun DependencyHandler.Glance() { + // Jetpack Glance (Compose Widgets) + dependency("androidx.glance:glance-appwidget:${Versions.composeGlance}", api = false) +} + /** * Compose Window Insets + extras * https://github.com/google/accompanist @@ -251,7 +261,7 @@ fun DependencyHandler.Google() { //URL: https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-play-services implementation( - "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.3}" + "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.3" ) Billing(api = false) @@ -417,12 +427,16 @@ fun DependencyHandler.ThirdParty() { //URL: https://github.com/jeziellago/compose-markdown implementation("com.github.jeziellago:compose-markdown:0.2.6") + Keval() EventBus() + OpenCSV() +} + +fun DependencyHandler.Keval() { //URL: https://github.com/notKamui/Keval - evaluate math expressions (calculator) + // TODO: Remove keval because we're using our own `:parser` + `:math` implementation("com.notkamui.libs:keval:0.8.0") - - OpenCSV() } fun DependencyHandler.OpenCSV() { @@ -469,7 +483,7 @@ fun DependencyHandler.Testing( // Robolectric doesn't integrate well with JUnit5 and Kotest // Robolectric(api = false) - if(commonTest) { + if (commonTest) { testImplementation(project(":common:test")) } if (commonAndroidTest) { @@ -509,6 +523,7 @@ fun DependencyHandler.Kotest() { androidTestDependency("io.kotest:kotest-assertions-core:${Versions.kotest}", api = api) testDependency("io.kotest:kotest-property:${Versions.kotest}", api = api) + testDependency("io.kotest:kotest-framework-datatest:${Versions.kotest}", api = api) testDependency("io.kotest:kotest-framework-api-jvm:${Versions.kotest}", api = api) testImplementation("io.kotest:kotest-framework-engine-jvm:${Versions.kotest}") diff --git a/categories/build.gradle.kts b/categories/build.gradle.kts index 11ef563d20..e2a355b3fd 100644 --- a/categories/build.gradle.kts +++ b/categories/build.gradle.kts @@ -1,4 +1,5 @@ import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing apply() @@ -8,14 +9,11 @@ plugins { dependencies { Hilt() - implementation(project(":common")) + implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) + implementation(project(":core:domain")) implementation(project(":core:ui")) implementation(project(":core:data-model")) implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) + Testing() } \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/CategoriesBottomBar.kt b/categories/src/main/java/com/ivy/categories/CategoriesBottomBar.kt deleted file mode 100644 index 56bb0c3f90..0000000000 --- a/categories/src/main/java/com/ivy/categories/CategoriesBottomBar.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.ivy.categories - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Blue -import com.ivy.wallet.ui.theme.components.BackBottomBar -import com.ivy.wallet.ui.theme.components.IvyButton - -@Composable -internal fun BoxWithConstraintsScope.CategoriesBottomBar( - onClose: () -> Unit, - onAddCategory: () -> Unit -) { - BackBottomBar(onBack = onClose) { - IvyButton( - text = stringResource(R.string.add_category), - iconStart = R.drawable.ic_plus - ) { - onAddCategory() - } - } -} - -@Preview -@Composable -private fun PreviewBottomBar() { - IvyPreview { - Column( - Modifier - .fillMaxSize() - .background(Blue) - ) { - - } - - CategoriesBottomBar( - onAddCategory = {}, - onClose = {} - ) - } -} diff --git a/categories/src/main/java/com/ivy/categories/CategoriesEvent.kt b/categories/src/main/java/com/ivy/categories/CategoriesEvent.kt new file mode 100644 index 0000000000..8bb86ea62e --- /dev/null +++ b/categories/src/main/java/com/ivy/categories/CategoriesEvent.kt @@ -0,0 +1,7 @@ +package com.ivy.categories + +import com.ivy.core.ui.data.CategoryUi + +sealed interface CategoriesEvent { + data class CategoryClick(val category: CategoryUi) : CategoriesEvent +} \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt b/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt index 47fe87531d..c756eb3385 100644 --- a/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt +++ b/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt @@ -1,582 +1,223 @@ package com.ivy.categories -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text import androidx.compose.runtime.* 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.SolidColor -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.R -import com.ivy.base.SortOrder -import com.ivy.data.CategoryOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style +import com.ivy.categories.component.categoriesList +import com.ivy.categories.data.CategoryListItemUi +import com.ivy.categories.data.CategoryListItemUi.ParentCategory +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.category.create.CreateCategoryModal +import com.ivy.core.ui.category.edit.EditCategoryModal +import com.ivy.core.ui.category.reorder.ReorderCategoriesModal +import com.ivy.core.ui.component.ScreenBottomBar +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.period.SelectedPeriodUi +import com.ivy.core.ui.data.period.dummyRangeUi +import com.ivy.core.ui.time.PeriodButton +import com.ivy.core.ui.time.PeriodModal +import com.ivy.design.l0_system.color.Blue +import com.ivy.design.l0_system.color.Green +import com.ivy.design.l0_system.color.Purple2Dark +import com.ivy.design.l0_system.color.Red +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l1_buildingBlocks.SpacerWeight +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.ReorderButton +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.IvyPreview - - -import com.ivy.wallet.ui.category.CategoriesScreenEvent -import com.ivy.wallet.ui.category.CategoriesScreenState -import com.ivy.wallet.ui.category.CategoriesViewModel -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.* -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.ui.theme.modal.ModalSet -import com.ivy.wallet.ui.theme.modal.ModalTitle -import com.ivy.wallet.ui.theme.modal.edit.CategoryModal -import com.ivy.wallet.ui.theme.modal.edit.CategoryModalData -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 -import com.ivy.wallet.utils.balancePrefix -import java.util.* +import com.ivy.resources.R @Composable -fun BoxWithConstraintsScope.CategoriesScreen() { +fun BoxScope.CategoriesScreen() { val viewModel: CategoriesViewModel = hiltViewModel() - val state by viewModel.state().collectAsState() + val state by viewModel.uiState.collectAsState() - UI( - state = state, - onEvent = viewModel::onEvent - ) + UI(state = state, onEvent = viewModel::onEvent) } @Composable -private fun BoxWithConstraintsScope.UI( - state: CategoriesScreenState = CategoriesScreenState(), - onEvent: (CategoriesScreenEvent) -> Unit = {} +private fun BoxScope.UI( + state: CategoriesState, + onEvent: (CategoriesEvent) -> Unit, ) { - + val periodModal = rememberIvyModal() + val createCategoryModal = rememberIvyModal() + var editCategoryId by remember { mutableStateOf(null) } + val editCategoryModal = rememberIvyModal() + val reorderModal = rememberIvyModal() LazyColumn( modifier = Modifier .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding(), + .systemBarsPadding(), ) { - item { - Spacer(Modifier.height(32.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - Text( - text = stringResource(R.string.categories), - style = UI.typo.h2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.weight(1f)) - - CircleButtonFilled( - icon = R.drawable.ic_sort_by_alpha_24, - onClick = { - onEvent( - CategoriesScreenEvent.OnSortOrderModalVisible( - visible = true - ) - ) - }, - clickAreaPadding = 12.dp - ) - - Spacer(modifier = Modifier.width(16.dp)) - - ReorderButton { - onEvent(CategoriesScreenEvent.OnReorderModalVisible(true)) + item(key = "header") { + SpacerVer(height = 16.dp) + Header( + selectedPeriodUi = state.selectedPeriod, + periodModal = periodModal, + onReorder = { + reorderModal.show() } - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.height(16.dp)) - } - - items( - items = state.categories, - key = { it.category.id } - ) { categoryData -> - CategoryCard( - currency = state.baseCurrency, - categoryData = categoryData, - onLongClick = { - CategoriesScreenEvent.OnReorderModalVisible(true) - } - ) { -// nav.navigateTo( -// ItemStatistic( -// accountId = null, -// categoryId = categoryData.category.id -// ) -// ) - } - } - - item { - Spacer(Modifier.height(150.dp)) //scroll hack - } - } - CategoriesBottomBar( - onAddCategory = { - onEvent( - CategoriesScreenEvent.OnCategoryModalVisible( - CategoryModalData(category = null) - ) ) - }, - onClose = { - - }, - ) - - ReorderModalSingleType( - visible = state.reorderModalVisible, - initialItems = state.categories, - dismiss = { - onEvent(CategoriesScreenEvent.OnReorderModalVisible(false)) - }, - onReordered = { - onEvent(CategoriesScreenEvent.OnReorder(it)) - } - ) { _, item -> - Text( - modifier = Modifier - .fillMaxWidth() - .padding(end = 24.dp) - .padding(vertical = 8.dp), - text = item.category.name, - style = UI.typo.b1.style( - color = item.category.color.toComposeColor(), - fontWeight = FontWeight.Bold - ) - ) - } - - CategoryModal( - modal = state.categoryModalData, - parentCategoryList = state.parentCategoryList, - onCreateCategory = { - onEvent(CategoriesScreenEvent.OnCreateCategory(it)) - }, - onEditCategory = { }, - dismiss = { - onEvent(CategoriesScreenEvent.OnCategoryModalVisible(null)) - } - ) - - SortModal( - initialType = state.sortOrder, - items = state.sortOrderItems, - visible = state.sortModalVisible, - dismiss = { - onEvent(CategoriesScreenEvent.OnSortOrderModalVisible(visible = false)) - }, - onSortOrderChanged = { - onEvent(CategoriesScreenEvent.OnReorder(state.categories, it)) - } - ) -} - -@Composable -private fun CategoryCard( - currency: String, - categoryData: CategoryData, - onLongClick: () -> Unit, - onClick: () -> Unit -) { - val category = categoryData.category - val contrastColor = findContrastTextColor(category.color.toComposeColor()) - - Spacer(Modifier.height(16.dp)) - - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .clip(UI.shapes.squared) - .border(2.dp, UI.colors.medium, UI.shapes.squared) - .clickable( - onClick = onClick - ) - ) { - CategoryHeader( - categoryData = categoryData, - currency = currency, - contrastColor = contrastColor - ) - - Spacer(Modifier.height(12.dp)) - - AddedSpent( - currency = currency, - monthlyIncome = categoryData.monthlyIncome, - monthlyExpenses = categoryData.monthlyExpenses - ) - - Spacer(Modifier.height(12.dp)) - } -} - -@Composable -fun AddedSpent( - modifier: Modifier = Modifier, - textColor: Color = UI.colorsInverted.pure, - dividerColor: Color = UI.colors.medium, - monthlyIncome: Double, - monthlyExpenses: Double, - currency: String, - center: Boolean = true, - dividerSpacer: Dp? = null, -) { - Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - if (center) { - Spacer(Modifier.weight(1f)) - } - - LabelAmount( - textColor = textColor, - label = stringResource(R.string.month_expenses), - amount = monthlyExpenses, - currency = currency, - center = center - ) - - if (center) { - Spacer(Modifier.weight(1f)) - } - - if (dividerSpacer != null) { - Spacer(modifier = Modifier.width(dividerSpacer)) - } - - //Divider - Spacer( - modifier = Modifier - .width(2.dp) - .height(48.dp) - .background(dividerColor, UI.shapes.fullyRounded) - ) - - if (center) { - Spacer(Modifier.weight(1f)) - } - - if (dividerSpacer != null) { - Spacer(modifier = Modifier.width(dividerSpacer)) - } - - LabelAmount( - textColor = textColor, - label = stringResource(R.string.month_income), - amount = monthlyIncome, - currency = currency, - center = center + SpacerVer(height = 20.dp) + } + categoriesList( + items = state.items, + emptyState = state.emptyState, + onCategoryClick = { + editCategoryId = it.id + editCategoryModal.show() + }, + onParentCategoryClick = { + editCategoryId = it.id + editCategoryModal.show() + }, + onCreateCategory = { + createCategoryModal.show() + } ) - - if (center) { - Spacer(Modifier.weight(1f)) + item(key = "last_item_spacer") { + SpacerVer(height = 64.dp) } } -} - -@Composable -private fun LabelAmount( - label: String, - amount: Double, - currency: String, - textColor: Color, - center: Boolean -) { - Column( - horizontalAlignment = if (center) Alignment.CenterHorizontally else Alignment.Start - ) { - Text( - text = label, - style = UI.typo.c.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(4.dp)) - Row( - verticalAlignment = Alignment.CenterVertically + ScreenBottomBar { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.High, + feeling = Feeling.Positive, + text = "Add", + icon = R.drawable.ic_round_add_24 ) { - AmountCurrencyB1( - textColor = textColor, - amount = amount, - currency = currency - ) + createCategoryModal.show() } - } -} - -@Composable -private fun CategoryHeader( - categoryData: CategoryData, - currency: String, - contrastColor: Color, -) { - val category = categoryData.category - - Column( - modifier = Modifier - .fillMaxWidth() - .background(category.color.toComposeColor(), UI.shapes.squaredTop) - ) { - Spacer(Modifier.height(16.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - ItemIconSDefaultIcon( - iconName = category.icon, - defaultIcon = R.drawable.ic_custom_category_s, - tint = contrastColor - ) - - Spacer(Modifier.width(8.dp)) - - Text( - text = category.name, - style = UI.typo.b1.style( - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - ) - } - - Spacer(Modifier.height(4.dp)) - - BalanceRow( - modifier = Modifier.align(Alignment.CenterHorizontally), - - decimalPaddingTop = 4.dp, - textColor = contrastColor, - currency = currency, - balance = categoryData.monthlyBalance, - - integerFontSize = 30.sp, - decimalFontSize = 18.sp, - currencyFontSize = 30.sp, - - currencyUpfront = false, - balanceAmountPrefix = balancePrefix( - income = categoryData.monthlyIncome, - expenses = categoryData.monthlyExpenses - ) + state.selectedPeriod?.let { + PeriodModal( + modal = periodModal, + selectedPeriod = state.selectedPeriod ) - - Spacer(Modifier.height(16.dp)) } -} -@Composable -fun BoxWithConstraintsScope.SortModal( - title: String = stringResource(R.string.sort_by), - items: List, - visible: Boolean, - initialType: SortOrder, - id: UUID = UUID.randomUUID(), - dismiss: () -> Unit, - onSortOrderChanged: (SortOrder) -> Unit -) { - var sortOrder by remember(initialType) { - mutableStateOf(initialType) - } - - val applyChange = { - onSortOrderChanged(sortOrder) - dismiss() - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSet { - applyChange() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle(text = title) - - Spacer(Modifier.height(32.dp)) - - items.forEach { - SelectTypeButton( - text = it.displayName, - icon = when (it) { - SortOrder.DEFAULT -> R.drawable.ic_custom_star_s - SortOrder.BALANCE_AMOUNT -> R.drawable.ic_vue_money_coins - SortOrder.EXPENSES -> R.drawable.ic_expense - SortOrder.ALPHABETICAL -> R.drawable.ic_sort_by_alpha_24 - }, - selected = it == sortOrder - ) { - sortOrder = it - applyChange() - } - Spacer(Modifier.height(12.dp)) - } + CreateCategoryModal(modal = createCategoryModal) + editCategoryId?.let { + EditCategoryModal(modal = editCategoryModal, categoryId = it) } + ReorderCategoriesModal(modal = reorderModal) } @Composable -private fun SelectTypeButton( - text: String, - @DrawableRes icon: Int, - selected: Boolean, - selectedGradient: Gradient = GradientGreen, - textSelectedColor: Color = White, - onClick: () -> Unit +private fun Header( + selectedPeriodUi: SelectedPeriodUi?, + periodModal: IvyModal, + onReorder: () -> Unit, ) { Row( modifier = Modifier - .padding(horizontal = 16.dp) .fillMaxWidth() - .height(64.dp) - .clip(UI.shapes.squared) - .background( - brush = if (selected) selectedGradient.asHorizontalBrush() else SolidColor(UI.colors.medium), - shape = UI.shapes.squared - ) - .clickable { - onClick() - } - .padding(vertical = 16.dp), + .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(16.dp)) - - val textColor = if (selected) textSelectedColor else UI.colorsInverted.pure - - IvyIcon( - icon = icon, - tint = textColor, - modifier = Modifier.fillMaxHeight() - ) + if (selectedPeriodUi != null) { + PeriodButton(selectedPeriod = selectedPeriodUi, periodModal = periodModal) + } + SpacerWeight(weight = 1f) + ReorderButton(onClick = onReorder) + } +} - Spacer(Modifier.width(12.dp)) - Text( - modifier = Modifier.wrapContentHeight(), - text = text, - style = UI.typo.b1.style( - color = textColor +// region Preview +@Preview +@Composable +private fun Preview_Empty() { + IvyPreview { + UI( + state = CategoriesState( + selectedPeriod = SelectedPeriodUi.AllTime( + btnText = "All-time", + rangeUi = dummyRangeUi() + ), + items = emptyList(), + emptyState = true ), - textAlign = TextAlign.Center, + onEvent = {} ) - - if (selected) { - Spacer(Modifier.weight(1f)) - - IvyIcon( - icon = R.drawable.ic_check, - tint = textSelectedColor - ) - - Text( - text = stringResource(R.string.selected), - style = UI.typo.b2.style( - fontWeight = FontWeight.SemiBold, - color = textSelectedColor - ) - ) - - Spacer(Modifier.width(24.dp)) - } } } - @Preview @Composable private fun Preview() { IvyPreview { - val state = CategoriesScreenState( - baseCurrency = "BGN", - categories = listOf( - CategoryData( - category = CategoryOld( - "Groceries", - Green.toArgb(), - icon = "groceries" - ), - monthlyBalance = 2125.0, - monthlyExpenses = 920.0, - monthlyIncome = 3045.0 + UI( + state = CategoriesState( + selectedPeriod = SelectedPeriodUi.AllTime( + btnText = "Sep", + rangeUi = dummyRangeUi() ), - CategoryData( - category = CategoryOld( - "Fun", - Orange.toArgb(), - icon = "game" + items = listOf( + ParentCategory( + parentCategory = dummyCategoryUi("Business"), + balance = dummyValueUi("+3,320.50"), + categoryCards = listOf( + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Category 1"), + balance = dummyValueUi("-1,000.00"), + ), + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Category 2", color = Blue), + balance = dummyValueUi("0.00"), + ), + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Category 3", color = Red), + balance = dummyValueUi("+4,320.50"), + ), + ), + categoriesCount = 3, ), - monthlyBalance = 1200.0, - monthlyExpenses = 750.0, - monthlyIncome = 0.0 - ), - CategoryData( - category = CategoryOld("Ivy", IvyDark.toArgb()), - monthlyBalance = 1200.0, - monthlyExpenses = 0.0, - monthlyIncome = 5000.0 - ), - CategoryData( - category = CategoryOld( - "Food", - GreenLight.toArgb(), - icon = "atom" + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Tech", color = Purple2Dark), + balance = dummyValueUi("-30k"), ), - monthlyBalance = 12125.21, - monthlyExpenses = 1350.50, - monthlyIncome = 8000.48 - ), - CategoryData( - category = CategoryOld( - "Shisha", - GreenDark.toArgb(), - icon = "drink" + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Groceries", color = Green), + balance = dummyValueUi("-5,025.54"), ), - monthlyBalance = 820.0, - monthlyExpenses = 340.0, - monthlyIncome = 400.0 + CategoryListItemUi.Archived( + categoryCards = listOf( + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Category 1"), + balance = dummyValueUi("-1,000.00", "BGN"), + ), + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Category 2", color = Blue), + balance = dummyValueUi("0.00"), + ), + CategoryListItemUi.CategoryCard( + category = dummyCategoryUi("Account 3", color = Red), + balance = dummyValueUi("+4,320.50"), + ), + ), + count = 3, + ) ), - - ) + emptyState = false, + ), + onEvent = {} ) - UI(state = state) } -} \ No newline at end of file +} +// endregion \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/CategoriesState.kt b/categories/src/main/java/com/ivy/categories/CategoriesState.kt new file mode 100644 index 0000000000..fce34ac3a6 --- /dev/null +++ b/categories/src/main/java/com/ivy/categories/CategoriesState.kt @@ -0,0 +1,12 @@ +package com.ivy.categories + +import androidx.compose.runtime.Immutable +import com.ivy.categories.data.CategoryListItemUi +import com.ivy.core.ui.data.period.SelectedPeriodUi + +@Immutable +data class CategoriesState( + val selectedPeriod: SelectedPeriodUi?, + val items: List, + val emptyState: Boolean, +) \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt b/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt index 9256816371..b49f7c7d00 100644 --- a/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt +++ b/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt @@ -1,258 +1,136 @@ -package com.ivy.wallet.ui.category - -import androidx.lifecycle.viewModelScope -import com.ivy.base.SortOrder -import com.ivy.categories.CategoryData -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld -import com.ivy.frp.action.thenMap -import com.ivy.frp.test.TestIdlingResource -import com.ivy.frp.thenInvokeAfter -import com.ivy.frp.viewmodel.FRPViewModel -import com.ivy.wallet.domain.action.account.AccountsActOld -import com.ivy.wallet.domain.action.category.CategoriesActOld -import com.ivy.wallet.domain.action.category.CategoryIncomeWithAccountFiltersAct -import com.ivy.wallet.domain.action.settings.BaseCurrencyActOld -import com.ivy.wallet.domain.action.transaction.TrnsWithRangeAndAccFiltersAct -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.domain.deprecated.sync.item.CategorySync -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.CategoryDao -import com.ivy.wallet.io.persistence.data.toEntity -import com.ivy.wallet.ui.theme.modal.edit.CategoryModalData -import com.ivy.wallet.utils.ioThread -import com.ivy.wallet.utils.scopedIOThread +package com.ivy.categories + +import com.ivy.categories.data.CategoryListItemUi +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.calculate.Stats +import com.ivy.core.domain.action.calculate.category.CatStatsFlow +import com.ivy.core.domain.action.category.CategoriesListFlow +import com.ivy.core.domain.action.data.CategoryListItem +import com.ivy.core.domain.action.period.SelectedPeriodFlow +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.format +import com.ivy.core.domain.pure.time.range +import com.ivy.core.domain.pure.util.combineList +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.action.mapping.MapSelectedPeriodUiAct +import com.ivy.data.Value +import com.ivy.data.category.Category +import com.ivy.data.time.TimeRange import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import javax.inject.Inject -import kotlin.math.absoluteValue @HiltViewModel class CategoriesViewModel @Inject constructor( - private val categoryDao: CategoryDao, - private val categorySync: CategorySync, - private val categoryCreator: CategoryCreator, - private val categoriesAct: CategoriesActOld, - private val ivyContext: IvyWalletCtx, - private val sharedPrefs: SharedPrefs, - private val baseCurrencyAct: BaseCurrencyActOld, - private val accountsAct: AccountsActOld, - private val trnsWithRangeAndAccFiltersAct: TrnsWithRangeAndAccFiltersAct, - private val categoryIncomeWithAccountFiltersAct: CategoryIncomeWithAccountFiltersAct -) : FRPViewModel() { - - override val _state: MutableStateFlow = MutableStateFlow( - CategoriesScreenState() + private val categoriesListFlow: CategoriesListFlow, + private val selectedPeriodFlow: SelectedPeriodFlow, + private val mapSelectedPeriodUiAct: MapSelectedPeriodUiAct, + private val categoryStatsFlow: CatStatsFlow, + private val mapCategoryUiAct: MapCategoryUiAct, +) : SimpleFlowViewModel() { + override val initialUi = CategoriesState( + selectedPeriod = null, + items = listOf(), + emptyState = true, ) - override suspend fun handleEvent(event: Nothing): suspend () -> CategoriesScreenState { - TODO("Not yet implemented") - } - - private var allAccounts = emptyList() - private var baseCurrency = "" - private var transactions = emptyList() - - fun start() { - viewModelScope.launch(Dispatchers.IO) { - TestIdlingResource.increment() - - initialise() - loadCategories() - - TestIdlingResource.decrement() - } + override val uiFlow: Flow = combine( + selectedPeriodFlow(), categoryItemsUi() + ) { period, items -> + CategoriesState( + selectedPeriod = mapSelectedPeriodUiAct(period), + items = items, + emptyState = items.isEmpty(), + ) } - private suspend fun initialise() { - ioThread { - val range = TimePeriod.currentMonth( - startDayOfMonth = ivyContext.startDayOfMonth - ).toRange(ivyContext.startDayOfMonth) //this must be monthly - - allAccounts = accountsAct(Unit) - baseCurrency = baseCurrencyAct(Unit) - - transactions = trnsWithRangeAndAccFiltersAct( - TrnsWithRangeAndAccFiltersAct.Input( - range = range, - accountIdFilterSet = suspend { allAccounts } thenMap { it.id } - thenInvokeAfter { it.toHashSet() } - ) - ) - - val sortOrder = SortOrder.from( - sharedPrefs.getInt( - SharedPrefs.CATEGORY_SORT_ORDER, - SortOrder.DEFAULT.orderNum - ) - ) - - val parentCategoryList = categoryDao.findAllParentCategories().map { it.toDomain() } - - updateState { - it.copy(sortOrder = sortOrder, parentCategoryList = parentCategoryList) - } - } - } - - private suspend fun loadCategories() { - scopedIOThread { scope -> - val categories = categoriesAct(Unit).mapAsync(scope) { - val catIncomeExpense = categoryIncomeWithAccountFiltersAct( - CategoryIncomeWithAccountFiltersAct.Input( - transactions = transactions, - accountFilterList = allAccounts, - category = it, - baseCurrency = baseCurrency - ) - ) - - CategoryData( - category = it, - monthlyBalance = (catIncomeExpense.income - catIncomeExpense.expense).toDouble(), - monthlyIncome = catIncomeExpense.income.toDouble(), - monthlyExpenses = catIncomeExpense.expense.toDouble() - ) - } - - val sortedList = sortList(categories, stateVal().sortOrder) - - updateState { - it.copy(baseCurrency = baseCurrency, categories = sortedList) - } - } - } - - private suspend fun reorder( - newOrder: List, - sortOrder: SortOrder = SortOrder.DEFAULT - ) { - TestIdlingResource.increment() - - val sortedList = sortList(newOrder, sortOrder) - - if (sortOrder == SortOrder.DEFAULT) { - ioThread { - sortedList.forEachIndexed { index, categoryData -> - categoryDao.save( - categoryData.category.toEntity().copy( - orderNum = index.toDouble(), - isSynced = false + @OptIn(ExperimentalCoroutinesApi::class) + private fun categoryItemsUi(): Flow> = combine( + selectedPeriodFlow(), + categoriesListFlow(CategoriesListFlow.Input(trnType = null)) + ) { period, list -> + val range = period.range() + combineList(list.map { item -> + when (item) { + is CategoryListItem.Archived -> { + combineList( + item.categories.map { category -> + categoryCardFlow(category, range) + } + ).map { cards -> + CategoryListItemUi.Archived( + categoryCards = cards, + count = cards.size ) - ) + } + } + is CategoryListItem.CategoryHolder -> categoryCardFlow(item.category, range) + is CategoryListItem.ParentCategory -> { + combine( + categoryStatsFlow( + CatStatsFlow.Input( + category = item.parent, + range = range, + ) + ), + combineList(item.children.map { category -> + categoryStatsFlow( + CatStatsFlow.Input( + category = category, + range = range, + ) + ).map { stats -> + CategoryListItemUi.CategoryCard( + category = mapCategoryUiAct(category), + balance = format(stats.balance, shortenFiat = true), + ) to stats + } + }) + ) { parentStats, children -> + CategoryListItemUi.ParentCategory( + parentCategory = mapCategoryUiAct(item.parent), + balance = parentCategoryBalance( + parentStats, + children.map { it.second } + ), + categoryCards = children.map { it.first }, + categoriesCount = children.size + ) + } } - } - } - - ioThread { - sharedPrefs.putInt(SharedPrefs.CATEGORY_SORT_ORDER, sortOrder.orderNum) - } - - updateState { - it.copy(categories = sortedList, sortOrder = sortOrder) - } - ioThread { - categorySync.sync() - } + } + }).map { it } + }.flatMapLatest { it } - TestIdlingResource.decrement() + private fun parentCategoryBalance(parentStats: Stats, children: List): ValueUi { + val totalBalance = parentStats.balance.amount + children.sumOf { it.balance.amount } + return format(Value(totalBalance, parentStats.balance.currency), shortenFiat = true) } - private fun sortList( - categoryData: List, - sortOrder: SortOrder - ): List { - return when (sortOrder) { - SortOrder.DEFAULT -> categoryData.sortedBy { - it.category.orderNum - } - SortOrder.BALANCE_AMOUNT -> categoryData.sortedByDescending { - it.monthlyBalance.absoluteValue - } - SortOrder.ALPHABETICAL -> categoryData.sortedBy { - it.category.name - } - SortOrder.EXPENSES -> categoryData.sortedByDescending { - it.monthlyExpenses - } - } + private fun categoryCardFlow( + category: Category, range: TimeRange + ): Flow = categoryStatsFlow( + CatStatsFlow.Input( + category = category, + range = range + ) + ).map { stats -> + CategoryListItemUi.CategoryCard( + category = mapCategoryUiAct(category), + balance = format(stats.balance, shortenFiat = true), + ) } - private suspend fun createCategory(data: CreateCategoryData) { - TestIdlingResource.increment() - - categoryCreator.createCategory(data) { - loadCategories() - } - TestIdlingResource.decrement() + // region Event handling + override suspend fun handleEvent(event: CategoriesEvent) = when (event) { + is CategoriesEvent.CategoryClick -> handleCategoryClick(event) } - fun onEvent(event: CategoriesScreenEvent) { - viewModelScope.launch(Dispatchers.Default) { - when (event) { - is CategoriesScreenEvent.OnReorder -> reorder(event.newOrder, event.sortOrder) - is CategoriesScreenEvent.OnCreateCategory -> createCategory(event.createCategoryData) - is CategoriesScreenEvent.OnReorderModalVisible -> updateState { - it.copy( - reorderModalVisible = event.visible - ) - } - is CategoriesScreenEvent.OnSortOrderModalVisible -> updateState { - it.copy( - sortModalVisible = event.visible - ) - } - is CategoriesScreenEvent.OnCategoryModalVisible -> updateState { - it.copy( - categoryModalData = event.categoryModalData - ) - } - else -> {} - } - } + private fun handleCategoryClick(event: CategoriesEvent.CategoryClick) { + // TODO: Implement } -} - -data class CategoriesScreenState( - val baseCurrency: String = "", - val categories: List = emptyList(), - val reorderModalVisible: Boolean = false, - val categoryModalData: CategoryModalData? = null, - val sortModalVisible: Boolean = false, - val sortOrderItems: List = SortOrder.values().toList(), - val sortOrder: SortOrder = SortOrder.DEFAULT, - val parentCategoryList: List = emptyList() -) - -sealed class CategoriesScreenEvent { - data class OnReorder( - val newOrder: List, - val sortOrder: SortOrder = SortOrder.DEFAULT - ) : CategoriesScreenEvent() - - data class OnCreateCategory(val createCategoryData: CreateCategoryData) : - CategoriesScreenEvent() - - data class OnReorderModalVisible(val visible: Boolean) : CategoriesScreenEvent() - data class OnSortOrderModalVisible(val visible: Boolean) : CategoriesScreenEvent() - data class OnCategoryModalVisible(val categoryModalData: CategoryModalData?) : - CategoriesScreenEvent() -} - -suspend inline fun Iterable.mapAsync( - scope: CoroutineScope, - crossinline transform: suspend (T) -> R -): List { - return this.map { - scope.async { - transform(it) - } - }.awaitAll() + // endregion } diff --git a/categories/src/main/java/com/ivy/categories/CategoryData.kt b/categories/src/main/java/com/ivy/categories/CategoryData.kt deleted file mode 100644 index ebf3310d49..0000000000 --- a/categories/src/main/java/com/ivy/categories/CategoryData.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.ivy.categories - -import com.ivy.base.Reorderable -import com.ivy.data.CategoryOld - -data class CategoryData( - val category: CategoryOld, - val monthlyBalance: Double, - val monthlyExpenses: Double, - val monthlyIncome: Double -) : Reorderable { - override fun getItemOrderNum() = category.orderNum - - override fun withNewOrderNum(newOrderNum: Double) = this.copy( - category = category.copy( - orderNum = newOrderNum - ) - ) -} \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/component/ArchivedCategories.kt b/categories/src/main/java/com/ivy/categories/component/ArchivedCategories.kt new file mode 100644 index 0000000000..78feb018dd --- /dev/null +++ b/categories/src/main/java/com/ivy/categories/component/ArchivedCategories.kt @@ -0,0 +1,122 @@ +package com.ivy.categories.component + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.* +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.categories.R +import com.ivy.categories.data.CategoryListItemUi +import com.ivy.categories.data.CategoryListItemUi.CategoryCard +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.design.l0_system.color.Blue +import com.ivy.design.l0_system.color.Red +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.isInPreview + +@Composable +internal fun ArchivedCategories( + archived: CategoryListItemUi.Archived, + onCategoryClick: (CategoryUi) -> Unit, +) { + var expanded by if (isInPreview()) remember { + mutableStateOf(previewExpanded) + } else remember { mutableStateOf(false) } + ArchivedDivider( + expanded = expanded, + count = archived.count, + onSetExpanded = { expanded = it } + ) + AccountsList( + categories = archived.categoryCards, + expanded = expanded, + onCategoryClick = onCategoryClick + ) +} + +@Composable +private fun ArchivedDivider( + expanded: Boolean, + count: Int, + onSetExpanded: (Boolean) -> Unit +) { + IvyButton( + size = ButtonSize.Big, + visibility = Visibility.Low, + feeling = Feeling.Neutral, + text = "Archived ($count)", + icon = if (expanded) + R.drawable.round_expand_more_24 else R.drawable.ic_round_expand_less_24 + ) { + onSetExpanded(!expanded) + } +} + +@Composable +private fun AccountsList( + categories: List, + expanded: Boolean, + onCategoryClick: (CategoryUi) -> Unit, +) { + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column { + categories.forEach { item -> + key("archived_${item.category.id}") { + SpacerVer(height = 12.dp) + CategoryCard( + category = item.category, + balance = item.balance, + ) { + onCategoryClick(item.category) + } + } + } + } + } +} + + +// region Preview +private var previewExpanded = false + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + previewExpanded = true + Column { + ArchivedCategories( + archived = CategoryListItemUi.Archived( + categoryCards = listOf( + CategoryCard( + category = dummyCategoryUi("Category 1"), + balance = dummyValueUi("-1,000.00", "BGN"), + ), + CategoryCard( + category = dummyCategoryUi("Category 2", color = Blue), + balance = dummyValueUi("0.00"), + ), + CategoryCard( + category = dummyCategoryUi("Category 3", color = Red), + balance = dummyValueUi("+4,320.50"), + ), + ), + count = 3, + ), + onCategoryClick = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/component/CategoriesList.kt b/categories/src/main/java/com/ivy/categories/component/CategoriesList.kt new file mode 100644 index 0000000000..1918601625 --- /dev/null +++ b/categories/src/main/java/com/ivy/categories/component/CategoriesList.kt @@ -0,0 +1,131 @@ +package com.ivy.categories.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.categories.R +import com.ivy.categories.data.CategoryListItemUi +import com.ivy.categories.data.CategoryListItemUi.* +import com.ivy.core.ui.data.CategoryUi +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +fun LazyListScope.categoriesList( + items: List, + emptyState: Boolean, + onCategoryClick: (CategoryUi) -> Unit, + onParentCategoryClick: (CategoryUi) -> Unit, + onCreateCategory: () -> Unit, +) { + items( + items = items, + key = { + when (it) { + is Archived -> "archived" + is CategoryCard -> it.category.id + is ParentCategory -> it.parentCategory.id + } + } + ) { item -> + when (item) { + is CategoryCard -> { + SpacerVer(height = 8.dp) + CategoryCard( + category = item.category, + balance = item.balance, + onClick = { onCategoryClick(item.category) } + ) + } + is ParentCategory -> { + SpacerVer(height = 8.dp) + CategoryFolderCard( + parentCategory = item.parentCategory, + balance = item.balance, + categories = item.categoryCards, + categoriesCount = item.categoriesCount, + onCategoryClick = onCategoryClick, + onParentCategoryClick = { + onParentCategoryClick(item.parentCategory) + }, + ) + } + is Archived -> { + SpacerVer(height = 16.dp) + ArchivedCategories(archived = item, onCategoryClick = onCategoryClick) + } + } + } + + if (emptyState) { + item { + EmptyState(onCreateCategory = onCreateCategory) + } + } +} + +@Composable +private fun EmptyState( + onCreateCategory: () -> Unit +) { + SpacerVer(height = 96.dp) + B1( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = "No Categories", + textAlign = TextAlign.Center, + color = UI.colors.primary + ) + SpacerVer(height = 12.dp) + B2( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = "Create categories to better organize you transactions", + textAlign = TextAlign.Center + ) + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Focused, + feeling = Feeling.Positive, + text = "Create Category", + icon = R.drawable.ic_custom_category_s, + onClick = onCreateCategory, + ) + SpacerVer(height = 24.dp) +} + + +// region Preview +@Preview +@Composable +private fun Preview_EmptyState() { + ComponentPreview { + LazyColumn { + categoriesList( + items = emptyList(), + emptyState = false, + onCategoryClick = {}, + onCreateCategory = {}, + onParentCategoryClick = {}, + ) + } + } +} +// endregion \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/component/CategoryCard.kt b/categories/src/main/java/com/ivy/categories/component/CategoryCard.kt new file mode 100644 index 0000000000..5fb4765668 --- /dev/null +++ b/categories/src/main/java/com/ivy/categories/component/CategoryCard.kt @@ -0,0 +1,107 @@ +package com.ivy.categories.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.core.ui.value.AmountCurrency +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l0_system.color.rememberDynamicContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.util.ComponentPreview + +@Composable +internal fun CategoryCard( + category: CategoryUi, + balance: ValueUi, + onClick: () -> Unit, +) { + val dynamicContrast = rememberDynamicContrast(category.color) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(UI.shapes.rounded) + .border(1.dp, dynamicContrast, UI.shapes.rounded) + .clickable(onClick = onClick) + ) { + val contrast = rememberContrast(category.color) + Header( + icon = category.icon, + name = category.name, + color = category.color, + contrast = contrast, + ) + Balance(balance = balance) + } +} + +@Composable +private fun Header( + icon: ItemIcon, + name: String, + color: Color, + contrast: Color, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(color, UI.shapes.roundedTop) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ItemIcon(itemIcon = icon, size = IconSize.M, tint = contrast) + B2( + modifier = Modifier + .weight(1f) + .padding(start = 4.dp), + text = name, + color = contrast, + ) + } +} + +@Composable +private fun Balance( + balance: ValueUi +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + AmountCurrency(value = balance) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + CategoryCard( + category = dummyCategoryUi("Category"), + balance = dummyValueUi("-185.00"), + onClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/component/CategoryFolderCard.kt b/categories/src/main/java/com/ivy/categories/component/CategoryFolderCard.kt new file mode 100644 index 0000000000..7f10d8673c --- /dev/null +++ b/categories/src/main/java/com/ivy/categories/component/CategoryFolderCard.kt @@ -0,0 +1,244 @@ +package com.ivy.categories.component + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.categories.R +import com.ivy.categories.data.CategoryListItemUi.CategoryCard +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.core.ui.value.AmountCurrency +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.Blue +import com.ivy.design.l0_system.color.Red +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l0_system.color.rememberDynamicContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.isInPreview + + +@Composable +fun CategoryFolderCard( + parentCategory: CategoryUi, + balance: ValueUi, + categories: List, + categoriesCount: Int, + modifier: Modifier = Modifier, + onCategoryClick: (CategoryUi) -> Unit, + onParentCategoryClick: () -> Unit, +) { + val dynamicContrast = rememberDynamicContrast(parentCategory.color) + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(UI.shapes.rounded) + .border(1.dp, dynamicContrast, UI.shapes.rounded) + .clickable(onClick = onParentCategoryClick), + ) { + val contrastColor = rememberContrast(parentCategory.color) + Column( + modifier = Modifier + .fillMaxWidth() + .background(parentCategory.color, UI.shapes.roundedTop) + .padding(horizontal = 16.dp) + .padding(top = 4.dp, bottom = 12.dp) + ) { + IconNameRow( + folderName = parentCategory.name, + folderIcon = parentCategory.icon, + color = contrastColor + ) + SpacerVer(height = 2.dp) + Balance(balance = balance, color = contrastColor) + } + var expanded by if (isInPreview()) remember { + mutableStateOf(previewExpanded) + } else remember { mutableStateOf(false) } + ExpandCollapse( + expanded = expanded, + color = UI.colorsInverted.pure, + count = categoriesCount, + onSetExpanded = { expanded = it } + ) + Categories(expanded = expanded, items = categories, onClick = onCategoryClick) + } +} + +@Composable +private fun IconNameRow( + folderName: String, + folderIcon: ItemIcon, + color: Color, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + ItemIcon( + itemIcon = folderIcon, + size = IconSize.M, + tint = color, + ) + SpacerHor(width = 8.dp) + B2( + modifier = Modifier.weight(1f), + text = folderName, + color = color, + fontWeight = FontWeight.ExtraBold + ) + } +} + +@Composable +private fun Balance( + balance: ValueUi, + color: Color, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AmountCurrency(value = balance, color = color) + } +} + +@Composable +private fun ExpandCollapse( + expanded: Boolean, + color: Color, + count: Int, + onSetExpanded: (Boolean) -> Unit +) { + if (count > 0) { + IvyButton( + size = ButtonSize.Big, + shape = UI.shapes.roundedBottom, + visibility = Visibility.Low, + feeling = Feeling.Custom(color), + text = if (expanded) + "Tap to collapse ($count)" else "Tap to expand ($count)", + icon = if (expanded) + R.drawable.ic_round_expand_less_24 else R.drawable.round_expand_more_24 + ) { + onSetExpanded(!expanded) + } + } else { + B2( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + text = "Empty folder", + fontWeight = FontWeight.ExtraBold, + color = UI.colors.neutral, + textAlign = TextAlign.Center + ) + } + +} + +@Composable +private fun Categories( + expanded: Boolean, + items: List, + onClick: (CategoryUi) -> Unit +) { + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + Column(Modifier.fillMaxWidth()) { + items.forEach { + key(it.category.id) { + CategoryCard( + category = it.category, + balance = it.balance, + onClick = { onClick(it.category) } + ) + SpacerVer(height = 8.dp) + } + } + SpacerVer(height = 4.dp) + } + } +} + + +// region Preview +private var previewExpanded = false + +@Preview +@Composable +private fun Preview_Collapsed() { + ComponentPreview { + CategoryFolderCard( + parentCategory = dummyCategoryUi("Business"), + balance = dummyValueUi("5,320.50"), + categories = emptyList(), + categoriesCount = 0, + onCategoryClick = {}, + onParentCategoryClick = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_Expanded() { + ComponentPreview { + previewExpanded = true + CategoryFolderCard( + parentCategory = dummyCategoryUi("Business"), + balance = dummyValueUi("+3,320.50"), + categories = listOf( + CategoryCard( + category = dummyCategoryUi("Category 1"), + balance = dummyValueUi("-1,000.00"), + ), + CategoryCard( + category = dummyCategoryUi("Category 2", color = Blue), + balance = dummyValueUi("0.00"), + ), + CategoryCard( + category = dummyCategoryUi("Category 3", color = Red), + balance = dummyValueUi("+4,320.50"), + ), + ), + categoriesCount = 3, + onCategoryClick = {}, + onParentCategoryClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/categories/src/main/java/com/ivy/categories/data/CategoryListItemUi.kt b/categories/src/main/java/com/ivy/categories/data/CategoryListItemUi.kt new file mode 100644 index 0000000000..914416dcd8 --- /dev/null +++ b/categories/src/main/java/com/ivy/categories/data/CategoryListItemUi.kt @@ -0,0 +1,28 @@ +package com.ivy.categories.data + +import androidx.compose.runtime.Immutable +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.data.CategoryUi + +@Immutable +sealed interface CategoryListItemUi { + @Immutable + data class CategoryCard( + val category: CategoryUi, + val balance: ValueUi, + ) : CategoryListItemUi + + @Immutable + data class ParentCategory( + val parentCategory: CategoryUi, + val balance: ValueUi, + val categoryCards: List, + val categoriesCount: Int, + ) : CategoryListItemUi + + @Immutable + data class Archived( + val categoryCards: List, + val count: Int, + ) : CategoryListItemUi +} \ No newline at end of file diff --git a/common/main/src/main/java/com/ivy/common/CommonExt.kt b/common/main/src/main/java/com/ivy/common/CommonExt.kt index 2c4bc09f70..a815da6237 100644 --- a/common/main/src/main/java/com/ivy/common/CommonExt.kt +++ b/common/main/src/main/java/com/ivy/common/CommonExt.kt @@ -2,4 +2,14 @@ package com.ivy.common import java.util.* -fun String.toUUID(): UUID = UUID.fromString(this) \ No newline at end of file +fun String.toUUID(): UUID = UUID.fromString(this) + +fun String.toUUIDOrNull(): UUID? = try { + UUID.fromString(this) +} catch (e: Exception) { + null +} + +fun String?.isNotEmpty(): Boolean = !isNullOrEmpty() + +fun String?.isNotBlank(): Boolean = !isNullOrBlank() \ No newline at end of file diff --git a/common/main/src/main/java/com/ivy/common/Quadruple.kt b/common/main/src/main/java/com/ivy/common/Quadruple.kt new file mode 100644 index 0000000000..da1538181f --- /dev/null +++ b/common/main/src/main/java/com/ivy/common/Quadruple.kt @@ -0,0 +1,12 @@ +package com.ivy.common + +import java.io.Serializable + +data class Quadruple( + val first: A, + val second: B, + val third: C, + val fourth: D +) : Serializable { + override fun toString(): String = "($first, $second, $third, $fourth)" +} \ No newline at end of file diff --git a/common/main/src/main/java/com/ivy/common/StringUtil.kt b/common/main/src/main/java/com/ivy/common/StringUtil.kt new file mode 100644 index 0000000000..0bdf486acf --- /dev/null +++ b/common/main/src/main/java/com/ivy/common/StringUtil.kt @@ -0,0 +1,3 @@ +package com.ivy.common + +fun String?.isNotNullOrBlank(): Boolean = !isNullOrBlank() \ No newline at end of file diff --git a/common/main/src/main/java/com/ivy/common/di/CommonModuleDI.kt b/common/main/src/main/java/com/ivy/common/di/CommonModuleDI.kt index 9d3200a9a7..09490efd17 100644 --- a/common/main/src/main/java/com/ivy/common/di/CommonModuleDI.kt +++ b/common/main/src/main/java/com/ivy/common/di/CommonModuleDI.kt @@ -1,7 +1,7 @@ package com.ivy.common.di -import com.ivy.common.time.DeviceTimeProvider -import com.ivy.common.time.TimeProvider +import com.ivy.common.time.provider.DeviceTimeProvider +import com.ivy.common.time.provider.TimeProvider import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/common/main/src/main/java/com/ivy/common/time/CalendarPeriod.kt b/common/main/src/main/java/com/ivy/common/time/CalendarPeriod.kt new file mode 100644 index 0000000000..f1a9f0152e --- /dev/null +++ b/common/main/src/main/java/com/ivy/common/time/CalendarPeriod.kt @@ -0,0 +1,44 @@ +package com.ivy.common.time + +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.temporal.TemporalAdjusters + +// region Day +// .atStartOfDay() is already built-in in LocalDate + +fun LocalDate.atEndOfDay(): LocalDateTime = + this.atTime(23, 59, 59) +// endregion + +// region Week +fun startOfWeek(date: LocalDate): LocalDate = + date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) + +fun endOfWeek(date: LocalDate): LocalDate = + date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)) +// endregion + +// region Month +fun startOfMonth(date: LocalDate): LocalDate = + date.withDayOfMonth(1) + +fun endOfMonth(date: LocalDate): LocalDate = + date.withDayOfMonth(date.lengthOfMonth()) + +fun LocalDate.withDayOfMonthSafe(targetDayOfMonth: Int): LocalDate { + val maxDayOfMonth = this.lengthOfMonth() + return this.withDayOfMonth( + if (targetDayOfMonth > maxDayOfMonth) maxDayOfMonth else targetDayOfMonth + ) +} +// endregion + +// region Year +fun startOfYear(date: LocalDate): LocalDate = + LocalDate.of(date.year, 1, 1) + +fun endOfYear(date: LocalDate): LocalDate = + LocalDate.of(date.year, 12, 31) +// endregion diff --git a/common/main/src/main/java/com/ivy/common/time/TimeConversion.kt b/common/main/src/main/java/com/ivy/common/time/TimeConversion.kt index fa97006da9..cbe599eea7 100644 --- a/common/main/src/main/java/com/ivy/common/time/TimeConversion.kt +++ b/common/main/src/main/java/com/ivy/common/time/TimeConversion.kt @@ -1,5 +1,6 @@ package com.ivy.common.time +import com.ivy.common.time.provider.TimeProvider import java.time.Instant import java.time.LocalDateTime diff --git a/common/main/src/main/java/com/ivy/common/time/TimeExt.kt b/common/main/src/main/java/com/ivy/common/time/TimeExt.kt index 765a9e3ef8..faf83c2bcd 100644 --- a/common/main/src/main/java/com/ivy/common/time/TimeExt.kt +++ b/common/main/src/main/java/com/ivy/common/time/TimeExt.kt @@ -1,56 +1,70 @@ package com.ivy.common.time +import android.content.Context +import com.ivy.common.R +import com.ivy.common.time.provider.DeviceTimeProvider import java.time.* import java.time.format.DateTimeFormatter +import java.util.* // region Formatting fun LocalDateTime.format(pattern: String): String = this.format(DateTimeFormatter.ofPattern(pattern)) -fun LocalDate.format(pattern: String): String = +fun LocalTime.format(pattern: String): String = this.format(DateTimeFormatter.ofPattern(pattern)) -// endregion -// region Day -// .atStartOfDay() is already built-in in LocalDate - -fun LocalDate.atEndOfDay(): LocalDateTime = - this.atTime(23, 59, 59) -// endregion +fun LocalDate.format(pattern: String): String = + this.format(DateTimeFormatter.ofPattern(pattern)) -// region Week -// TODO +fun LocalTime.deviceFormat( + appContext: Context +): String = if (uses24HourFormat(appContext)) + format("HH:mm") else format("hh:mm a") // endregion -// region Month -fun startOfMonth(date: LocalDate): LocalDateTime = - date.withDayOfMonth(1).atStartOfDay() - -fun endOfMonth(date: LocalDate): LocalDateTime = - date.withDayOfMonth(date.lengthOfMonth()).atEndOfDay() - -fun LocalDate.withDayOfMonthSafe(targetDayOfMonth: Int): LocalDate { - val maxDayOfMonth = this.lengthOfMonth() - return this.withDayOfMonth( - if (targetDayOfMonth > maxDayOfMonth) maxDayOfMonth else targetDayOfMonth - ) +fun uses24HourFormat( + appContext: Context, +): Boolean = android.text.format.DateFormat.is24HourFormat(appContext) + +fun LocalDate.contextText( + alwaysShowWeekday: Boolean, + getString: (Int) -> String +): String { + val today = LocalDate.now() + val alwaysWeekdayText = if (alwaysShowWeekday) + " (${this.format(pattern = "EEEE")})" else "" + return when (this) { + today -> { + getString(R.string.today) + alwaysWeekdayText + } + today.minusDays(1) -> { + getString(R.string.yesterday) + alwaysWeekdayText + } + today.plusDays(1) -> { + getString(R.string.tomorrow) + alwaysWeekdayText + } + else -> { + this.format(pattern = "EEEE") + } + } } -// endregion -// region Year -// TODO -// endregion // region All-time -fun beginningOfIvyTime(): LocalDateTime = LocalDateTime.of(1990, 1, 1, 0, 0) +fun beginningOfIvyTime(): LocalDateTime = + LocalDateTime.of(1990, 1, 1, 0, 0) -fun endOfIvyTime(): LocalDateTime = LocalDateTime.of(2050, 1, 1, 0, 0) +fun endOfIvyTime(): LocalDateTime = + LocalDateTime.of(2050, 1, 1, 0, 0) // endregion -// region Deprecated (will be deleted) -@Deprecated("Don't use! Use TimeProvider via DI instead!") +fun LocalDate.dateId() = format("dd-MM-yyyy") + fun deviceTimeProvider() = DeviceTimeProvider() + +// region Deprecated (will be deleted) @Deprecated("Use `TimeProvider` instead!") fun timeNow(): LocalDateTime = LocalDateTime.now() diff --git a/common/main/src/main/java/com/ivy/common/time/DeviceTimeProvider.kt b/common/main/src/main/java/com/ivy/common/time/provider/DeviceTimeProvider.kt similarity index 91% rename from common/main/src/main/java/com/ivy/common/time/DeviceTimeProvider.kt rename to common/main/src/main/java/com/ivy/common/time/provider/DeviceTimeProvider.kt index 77b21615c2..67ef3b9c3f 100644 --- a/common/main/src/main/java/com/ivy/common/time/DeviceTimeProvider.kt +++ b/common/main/src/main/java/com/ivy/common/time/provider/DeviceTimeProvider.kt @@ -1,4 +1,4 @@ -package com.ivy.common.time +package com.ivy.common.time.provider import java.time.LocalDate import java.time.LocalDateTime diff --git a/common/main/src/main/java/com/ivy/common/time/TimeProvider.kt b/common/main/src/main/java/com/ivy/common/time/provider/TimeProvider.kt similarity index 84% rename from common/main/src/main/java/com/ivy/common/time/TimeProvider.kt rename to common/main/src/main/java/com/ivy/common/time/provider/TimeProvider.kt index 5e9bf8b386..ccd45a4b33 100644 --- a/common/main/src/main/java/com/ivy/common/time/TimeProvider.kt +++ b/common/main/src/main/java/com/ivy/common/time/provider/TimeProvider.kt @@ -1,4 +1,4 @@ -package com.ivy.common.time +package com.ivy.common.time.provider import java.time.LocalDate import java.time.LocalDateTime diff --git a/common/main/src/test/java/com/ivy/common/time/CalendarPeriodTest.kt b/common/main/src/test/java/com/ivy/common/time/CalendarPeriodTest.kt new file mode 100644 index 0000000000..2a9d270a27 --- /dev/null +++ b/common/main/src/test/java/com/ivy/common/time/CalendarPeriodTest.kt @@ -0,0 +1,137 @@ +package com.ivy.common.time + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.localDate +import io.kotest.property.checkAll +import java.time.LocalDate +import java.time.LocalDateTime + +class CalendarPeriodTest : StringSpec({ + // region Day + // the start of the day is already built-in so no need to test it + + "finds the end of the day" { + val date = LocalDate.of(2022, 5, 10) + + val res = date.atEndOfDay() + + res shouldBe LocalDateTime.of( + 2022, 5, 10, + 23, 59, 59 + ) + } + // endregion + + // region Week + "finds the start of the week" { + // Week 40 of 2022 + val monday = LocalDate.of(2022, 10, 3) + val sunday = LocalDate.of(2022, 10, 9) + + checkAll(Arb.localDate(monday, sunday)) { week40day -> + val res = startOfWeek(week40day) + + res shouldBe monday + } + } + + "finds the end of the week" { + // Week 40 of 2022 + val monday = LocalDate.of(2022, 10, 3) + val sunday = LocalDate.of(2022, 10, 9) + + checkAll(Arb.localDate(monday, sunday)) { week40day -> + val res = endOfWeek(week40day) + + res shouldBe sunday + } + } + // endregion + + // region Month + "finds the start of the month" { + checkAll( + Arb.localDate( + minDate = LocalDate.of(2022, 10, 1), + maxDate = LocalDate.of(2022, 10, 31), + ) + ) { dateInOctober -> + val res = startOfMonth(dateInOctober) + + res shouldBe LocalDate.of(2022, 10, 1) + } + } + + "finds the end of February" { + checkAll( + Arb.localDate( + minDate = LocalDate.of(2022, 2, 1), + maxDate = LocalDate.of(2022, 2, 28) + ) + ) { date -> + val res = endOfMonth(date) + + res shouldBe LocalDate.of(2022, 2, 28) + } + } + + "finds the end of February, leap year" { + checkAll( + Arb.localDate( + minDate = LocalDate.of(2024, 2, 1), + maxDate = LocalDate.of(2024, 2, 29) + ) + ) { date -> + val res = endOfMonth(date) + + res shouldBe LocalDate.of(2024, 2, 29) + } + } + + "finds the end of a 31 days month" { + checkAll( + Arb.localDate( + minDate = LocalDate.of(2024, 10, 1), + maxDate = LocalDate.of(2024, 10, 31) + ) + ) { date -> + val res = endOfMonth(date) + + res shouldBe LocalDate.of(2024, 10, 31) + } + } + + "finds the end of a 30 days month" { + checkAll( + Arb.localDate( + minDate = LocalDate.of(2024, 11, 1), + maxDate = LocalDate.of(2024, 11, 30) + ) + ) { date -> + val res = endOfMonth(date) + + res shouldBe LocalDate.of(2024, 11, 30) + } + } + // endregion + + // region Year + "finds the start of the year" { + checkAll(Arb.localDate()) { date -> + val res = startOfYear(date) + + res shouldBe LocalDate.of(date.year, 1, 1) + } + } + + "finds the end of the year" { + checkAll(Arb.localDate()) { date -> + val res = endOfYear(date) + + res shouldBe LocalDate.of(date.year, 12, 31) + } + } + // endregion +}) \ No newline at end of file diff --git a/common/main/src/test/java/com/ivy/common/time/TimeConversionTest.kt b/common/main/src/test/java/com/ivy/common/time/TimeConversionTest.kt index c92c75ea85..18e2aa0bfb 100644 --- a/common/main/src/test/java/com/ivy/common/time/TimeConversionTest.kt +++ b/common/main/src/test/java/com/ivy/common/time/TimeConversionTest.kt @@ -1,5 +1,6 @@ package com.ivy.common.time +import com.ivy.common.time.provider.TimeProvider import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.property.Arb diff --git a/common/test/src/main/java/com/ivy/common/test/TestTimeProvider.kt b/common/test/src/main/java/com/ivy/common/test/TestTimeProvider.kt index 97853e07fb..33c14f964a 100644 --- a/common/test/src/main/java/com/ivy/common/test/TestTimeProvider.kt +++ b/common/test/src/main/java/com/ivy/common/test/TestTimeProvider.kt @@ -1,6 +1,6 @@ package com.ivy.common.test -import com.ivy.common.time.TimeProvider +import com.ivy.common.time.provider.TimeProvider import com.ivy.common.time.toEpochMilli import com.ivy.common.time.toEpochSeconds import java.time.LocalDate diff --git a/core/data-model/src/main/java/com/ivy/data/IvyCurrency.kt b/core/data-model/src/main/java/com/ivy/data/IvyCurrency.kt index 83867b8004..5e39b655ac 100644 --- a/core/data-model/src/main/java/com/ivy/data/IvyCurrency.kt +++ b/core/data-model/src/main/java/com/ivy/data/IvyCurrency.kt @@ -4,7 +4,7 @@ import android.icu.util.Currency import java.util.* data class IvyCurrency( - val code: String, + val code: CurrencyCode, val name: String, val isCrypto: Boolean ) { diff --git a/core/data-model/src/main/java/com/ivy/data/Settings.kt b/core/data-model/src/main/java/com/ivy/data/Settings.kt index 19119dac97..bb887a5e67 100644 --- a/core/data-model/src/main/java/com/ivy/data/Settings.kt +++ b/core/data-model/src/main/java/com/ivy/data/Settings.kt @@ -5,7 +5,7 @@ import java.util.* @Deprecated("won't use") data class Settings( - val theme: Theme, + val theme: ThemeOld, val baseCurrency: CurrencyCode, val bufferAmount: BigDecimal, val name: String, diff --git a/core/data-model/src/main/java/com/ivy/data/Theme.kt b/core/data-model/src/main/java/com/ivy/data/Theme.kt index e542ac4445..583d01611f 100644 --- a/core/data-model/src/main/java/com/ivy/data/Theme.kt +++ b/core/data-model/src/main/java/com/ivy/data/Theme.kt @@ -1,14 +1,12 @@ package com.ivy.data -@Deprecated("move in design-system") -enum class Theme { - LIGHT, DARK, AUTO; +import androidx.compose.runtime.Immutable - fun inverted(): Theme { - return when (this) { - LIGHT -> DARK - DARK -> LIGHT - AUTO -> AUTO - } +@Immutable +enum class Theme(val code: Int) { + Light(1), Dark(-1), Auto(0); + + companion object { + fun fromCode(code: Int): Theme = values().first { it.code == code } } } \ No newline at end of file diff --git a/core/data-model/src/main/java/com/ivy/data/ThemeOld.kt b/core/data-model/src/main/java/com/ivy/data/ThemeOld.kt new file mode 100644 index 0000000000..f83c5dddc9 --- /dev/null +++ b/core/data-model/src/main/java/com/ivy/data/ThemeOld.kt @@ -0,0 +1,14 @@ +package com.ivy.data + +@Deprecated("move in design-system") +enum class ThemeOld { + LIGHT, DARK, AUTO; + + fun inverted(): ThemeOld { + return when (this) { + LIGHT -> DARK + DARK -> LIGHT + AUTO -> AUTO + } + } +} \ No newline at end of file diff --git a/core/data-model/src/main/java/com/ivy/data/account/Folder.kt b/core/data-model/src/main/java/com/ivy/data/account/Folder.kt new file mode 100644 index 0000000000..832cd87977 --- /dev/null +++ b/core/data-model/src/main/java/com/ivy/data/account/Folder.kt @@ -0,0 +1,11 @@ +package com.ivy.data.account + +import com.ivy.data.ItemIconId + +data class Folder( + val id: String, + val name: String, + val icon: ItemIconId?, + val color: Int, + val orderNum: Double, +) \ No newline at end of file diff --git a/core/data-model/src/main/java/com/ivy/data/category/CategoryType.kt b/core/data-model/src/main/java/com/ivy/data/category/CategoryType.kt index 1a0fdd3fdd..5eaf5e56c8 100644 --- a/core/data-model/src/main/java/com/ivy/data/category/CategoryType.kt +++ b/core/data-model/src/main/java/com/ivy/data/category/CategoryType.kt @@ -1,7 +1,7 @@ package com.ivy.data.category enum class CategoryType(val code: Int) { - Income(1), Expense(2), Both(2); + Income(1), Expense(2), Both(3); companion object { fun fromCode(code: Int) = values().firstOrNull { it.code == code } diff --git a/core/data-model/src/main/java/com/ivy/data/exchange/ExchangeProvider.kt b/core/data-model/src/main/java/com/ivy/data/exchange/ExchangeProvider.kt index c31903d751..238162d8bf 100644 --- a/core/data-model/src/main/java/com/ivy/data/exchange/ExchangeProvider.kt +++ b/core/data-model/src/main/java/com/ivy/data/exchange/ExchangeProvider.kt @@ -1,7 +1,8 @@ package com.ivy.data.exchange enum class ExchangeProvider(val code: Int) { - Coinbase(1); + Old(1), + Fawazahmed0(2); companion object { fun fromCode(code: Int): ExchangeProvider? = diff --git a/core/data-model/src/main/java/com/ivy/data/time/TimePeriod.kt b/core/data-model/src/main/java/com/ivy/data/time/TimePeriod.kt new file mode 100644 index 0000000000..a13cb01893 --- /dev/null +++ b/core/data-model/src/main/java/com/ivy/data/time/TimePeriod.kt @@ -0,0 +1,6 @@ +package com.ivy.data.time + +sealed interface TimePeriod { + data class Dynamic(val dynamic: DynamicTimePeriod) : TimePeriod + data class Fixed(val range: TimeRange) : TimePeriod +} \ No newline at end of file diff --git a/core/data-model/src/main/java/com/ivy/data/transaction/DueSection.kt b/core/data-model/src/main/java/com/ivy/data/transaction/DueSection.kt index cb7600af82..6188035304 100644 --- a/core/data-model/src/main/java/com/ivy/data/transaction/DueSection.kt +++ b/core/data-model/src/main/java/com/ivy/data/transaction/DueSection.kt @@ -5,5 +5,5 @@ import com.ivy.data.Value data class DueSection( val income: Value, val expense: Value, - val trns: List, + val trns: List, ) \ No newline at end of file diff --git a/core/data-model/src/main/java/com/ivy/data/transaction/TrnListItem.kt b/core/data-model/src/main/java/com/ivy/data/transaction/TrnListItem.kt index 6cdbbf6d03..9693181b57 100644 --- a/core/data-model/src/main/java/com/ivy/data/transaction/TrnListItem.kt +++ b/core/data-model/src/main/java/com/ivy/data/transaction/TrnListItem.kt @@ -15,8 +15,10 @@ sealed interface TrnListItem { ) : TrnListItem data class DateDivider( + val id: String, val date: LocalDate, val cashflow: Value, + val collapsed: Boolean, ) : TrnListItem } diff --git a/core/data-model/src/main/java/com/ivy/data/transaction/TrnTime.kt b/core/data-model/src/main/java/com/ivy/data/transaction/TrnTime.kt index ac6716639e..01d4c4a07c 100644 --- a/core/data-model/src/main/java/com/ivy/data/transaction/TrnTime.kt +++ b/core/data-model/src/main/java/com/ivy/data/transaction/TrnTime.kt @@ -5,4 +5,12 @@ import java.time.LocalDateTime sealed interface TrnTime { data class Actual(val actual: LocalDateTime) : TrnTime data class Due(val due: LocalDateTime) : TrnTime -} \ No newline at end of file +} + +fun dummyTrnTimeActual( + time: LocalDateTime = LocalDateTime.now() +) = TrnTime.Actual(time) + +fun dummyTrnTimeDue( + time: LocalDateTime = LocalDateTime.now().plusHours(1), +) = TrnTime.Due(time) \ No newline at end of file diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index b2960af6b8..6a2c1b66f6 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -13,11 +13,8 @@ plugins { dependencies { Hilt() implementation(project(":common:main")) - implementation(project(":temp-persistence")) implementation(project(":core:persistence")) implementation(project(":core:exchange-provider")) - implementation(project(":sync:public")) - implementation(project(":app-base")) // TODO: migrate to :resources Lifecycle(api = false) ComposeTesting(api = false) // for IdlingResource diff --git a/core/domain/src/androidTest/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlowTest.kt b/core/domain/src/androidTest/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlowTest.kt index 7360ea762c..9871288df0 100644 --- a/core/domain/src/androidTest/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlowTest.kt +++ b/core/domain/src/androidTest/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlowTest.kt @@ -1,7 +1,7 @@ package com.ivy.core.domain.action.calculate.transaction -import com.ivy.common.time.dateNowUTC -import com.ivy.common.time.timeNow +import com.ivy.common.test.testTimeProvider +import com.ivy.common.time.dateId import com.ivy.core.domain.action.exchange.SyncExchangeRatesAct import com.ivy.core.domain.action.settings.basecurrency.WriteBaseCurrencyAct import com.ivy.core.domain.pure.dummy.dummyTrn @@ -25,7 +25,7 @@ import javax.inject.Inject class GroupTrnsFlowTest { @get:Rule - var hiltRule = HiltAndroidRule(this) + val hiltRule = HiltAndroidRule(this) @Inject lateinit var groupTrnsFlow: GroupTrnsFlow @@ -44,7 +44,7 @@ class GroupTrnsFlowTest { @Test fun trn_history_with_2_transactions() = runBlocking { // Arrange - val now = timeNow() + val now = testTimeProvider().timeNow() val trn1 = dummyTrn( type = TransactionType.Expense, amount = 10.0, currency = "USD", time = TrnTime.Actual(now) @@ -54,7 +54,7 @@ class GroupTrnsFlowTest { time = TrnTime.Actual(now.minusSeconds(10)) ) writeBaseCurrencyAct("USD") - syncExchangeRatesAct(Unit) + syncExchangeRatesAct("USD") // Act val res = groupTrnsFlow(listOf(trn1, trn2)).take(2).last() @@ -65,8 +65,10 @@ class GroupTrnsFlowTest { overdue = null, history = listOf( TrnListItem.DateDivider( - date = dateNowUTC(), - cashflow = Value(amount = -5.0, currency = "USD") + id = testTimeProvider().dateNow().dateId(), + date = testTimeProvider().dateNow(), + cashflow = Value(amount = -5.0, currency = "USD"), + collapsed = false, ), TrnListItem.Trn(trn1), TrnListItem.Trn(trn2), diff --git a/core/domain/src/androidTest/java/com/ivy/core/domain/action/exchange/ExchangeRatesFlowTest.kt b/core/domain/src/androidTest/java/com/ivy/core/domain/action/exchange/ExchangeRatesFlowTest.kt new file mode 100644 index 0000000000..7bc9d5942c --- /dev/null +++ b/core/domain/src/androidTest/java/com/ivy/core/domain/action/exchange/ExchangeRatesFlowTest.kt @@ -0,0 +1,65 @@ +package com.ivy.core.domain.action.exchange + +import com.ivy.core.domain.action.settings.basecurrency.WriteBaseCurrencyAct +import com.ivy.data.exchange.ExchangeRatesData +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.* +import javax.inject.Inject + +@HiltAndroidTest +class ExchangeRatesFlowTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var exchangeRatesFlow: ExchangeRatesFlow + + @Inject + lateinit var writeBaseCurrencyAct: WriteBaseCurrencyAct + + @Inject + lateinit var syncExchangeRatesAct: SyncExchangeRatesAct + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + hiltRule.inject() + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Ignore("integration tests are broken") + @Test + fun fetches_exchange_rates_for_usd(): Unit = runBlocking { + // Arrange + writeBaseCurrencyAct("USD") + syncExchangeRatesAct("USD") + + // Act + val res = exchangeRatesFlow().take(2).toList() + + // Assert + res.first() shouldBe ExchangeRatesData( + baseCurrency = "", rates = emptyMap() + ) + res[1].baseCurrency shouldBe "USD" + res[1].rates.size shouldBeGreaterThan 0 + println("rates = ${res[1].rates}") + } +} \ No newline at end of file diff --git a/core/domain/src/main/AndroidManifest.xml b/core/domain/src/main/AndroidManifest.xml index f8f2cab9b5..6efac46640 100644 --- a/core/domain/src/main/AndroidManifest.xml +++ b/core/domain/src/main/AndroidManifest.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/FlowViewModel.kt b/core/domain/src/main/java/com/ivy/core/domain/FlowViewModel.kt index 07502b8a39..f26b3b73d9 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/FlowViewModel.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/FlowViewModel.kt @@ -3,63 +3,55 @@ package com.ivy.core.domain import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -abstract class FlowViewModel : ViewModel() { +abstract class FlowViewModel : ViewModel() { private val events = MutableSharedFlow(replay = 0) - protected abstract fun initialState(): State + protected abstract val initialState: InternalState + protected abstract val initialUi: UiState - protected abstract fun initialUiState(): UiState - - protected abstract fun stateFlow(): Flow - - protected abstract suspend fun mapToUiState(state: State): UiState + protected abstract val stateFlow: Flow + protected abstract val uiFlow: Flow protected abstract suspend fun handleEvent(event: Event) - private var stateFlow: StateFlow? = null - private var uiStateFlow: StateFlow? = null - - protected val state: StateFlow - get() = stateFlow ?: run { - stateFlow = stateFlow() - .flowOn(Dispatchers.Default) - .onEach { - Timber.d("State = $it") - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000L), - initialValue = initialState(), - ) - stateFlow!! - } + protected val state: StateFlow by lazy { + stateFlow + .flowOn(Dispatchers.Default) + .onEach { + Timber.d("Internal state = $it") + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = initialState, + ) + } - val uiState: StateFlow - get() = uiStateFlow ?: run { - uiStateFlow = state - .map { - mapToUiState(it) - } - .onEach { - Timber.d("UI state = $it") - } - .flowOn(Dispatchers.Default) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000L), - initialValue = initialUiState(), - ) - uiStateFlow!! - } + val uiState: StateFlow by lazy { + uiFlow.onEach { + Timber.d("UI state = $it") + }.flowOn(Dispatchers.Default) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000L), + initialValue = initialUi, + ) + } init { viewModelScope.launch { events.collect(::handleEvent) } + viewModelScope.launch { + // without this delay it crashes because isn't instantiated + delay(100) + state // init the lazy val for the internal state + } } fun onEvent(event: Event) { diff --git a/core/domain/src/main/java/com/ivy/core/domain/SimpleFlowViewModel.kt b/core/domain/src/main/java/com/ivy/core/domain/SimpleFlowViewModel.kt new file mode 100644 index 0000000000..e51bce3c8a --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/SimpleFlowViewModel.kt @@ -0,0 +1,9 @@ +package com.ivy.core.domain + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +abstract class SimpleFlowViewModel : FlowViewModel() { + override val initialState = Unit + override val stateFlow: Flow = flow {} +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/Action.kt b/core/domain/src/main/java/com/ivy/core/domain/action/Action.kt index 7882dbf48c..419b22e5de 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/Action.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/Action.kt @@ -4,12 +4,12 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -abstract class Action { - abstract suspend fun I.willDo(): O +abstract class Action { + abstract suspend fun Input.willDo(): Output protected open fun dispatcher(): CoroutineDispatcher = Dispatchers.IO - suspend operator fun invoke(input: I): O = withContext(dispatcher()) { + suspend operator fun invoke(input: Input): Output = withContext(dispatcher()) { input.willDo() } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountByIdAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountByIdAct.kt new file mode 100644 index 0000000000..a3c5b3f800 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountByIdAct.kt @@ -0,0 +1,13 @@ +package com.ivy.core.domain.action.account + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.dao.account.AccountDao +import com.ivy.data.account.Account +import javax.inject.Inject + +class AccountByIdAct @Inject constructor( + private val accountDao: AccountDao +) : Action() { + override suspend fun String.willDo(): Account? = + accountDao.findById(this)?.let(::toDomain) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountsAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountsAct.kt new file mode 100644 index 0000000000..7780f4f009 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountsAct.kt @@ -0,0 +1,14 @@ +package com.ivy.core.domain.action.account + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.dao.account.AccountDao +import com.ivy.data.account.Account +import javax.inject.Inject + +class AccountsAct @Inject constructor( + private val accountDao: AccountDao +) : Action>() { + override suspend fun Unit.willDo(): List = + accountDao.findAllSnapshot() + .map { acc -> toDomain(acc) } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountsFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountsFlow.kt index 1130c1dc9f..94538f88c5 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountsFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/AccountsFlow.kt @@ -24,20 +24,21 @@ class AccountsFlow @Inject constructor( override fun createFlow(): Flow> = accountDao.findAll().map { entities -> - entities.map { toAccount(acc = it) } + entities.map(::toDomain) }.flowOn(Dispatchers.IO) - private fun toAccount(acc: AccountEntity): Account = - Account( - id = acc.id.toUUID(), - name = acc.name, - currency = acc.currency, - color = acc.color, - icon = acc.icon, - excluded = acc.excluded, - folderId = acc.folderId?.toUUID(), - orderNum = acc.orderNum, - state = acc.state, - sync = acc.sync - ) -} \ No newline at end of file +} + +fun toDomain(acc: AccountEntity): Account = + Account( + id = acc.id.toUUID(), + name = acc.name, + currency = acc.currency, + color = acc.color, + icon = acc.icon, + excluded = acc.excluded, + folderId = acc.folderId?.toUUID(), + orderNum = acc.orderNum, + state = acc.state, + sync = acc.sync + ) \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/AdjustAccBalanceAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/AdjustAccBalanceAct.kt index bece9744bb..e77ec62dce 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/account/AdjustAccBalanceAct.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/AdjustAccBalanceAct.kt @@ -1,5 +1,6 @@ package com.ivy.core.domain.action.account +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.domain.action.Action import com.ivy.core.domain.action.calculate.account.AccBalanceFlow import com.ivy.core.domain.action.data.Modify @@ -16,6 +17,7 @@ import javax.inject.Inject class AdjustAccBalanceAct @Inject constructor( private val writeTrnsAct: WriteTrnsAct, private val accBalanceFlow: AccBalanceFlow, + private val timeProvider: TimeProvider, ) : Action() { /** * @param hideTransaction whether to hide the adjust transactions @@ -30,6 +32,7 @@ class AdjustAccBalanceAct @Inject constructor( val accBalance = accBalanceFlow(AccBalanceFlow.Input(account = account)).first() val adjustTrn = adjustBalanceTrn( + timeProvider = timeProvider, account = account, currentBalance = accBalance.amount, desiredBalance = desiredBalance, diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/NewAccountTabItemOrderNumAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/NewAccountTabItemOrderNumAct.kt new file mode 100644 index 0000000000..f375dc4f0e --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/NewAccountTabItemOrderNumAct.kt @@ -0,0 +1,18 @@ +package com.ivy.core.domain.action.account + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.dao.account.AccountDao +import com.ivy.core.persistence.dao.account.AccountFolderDao +import javax.inject.Inject + +class NewAccountTabItemOrderNumAct @Inject constructor( + private val accountDao: AccountDao, + private val folderDao: AccountFolderDao, +) : Action() { + override suspend fun Unit.willDo(): Double = currentMax() + 1 + + private suspend fun currentMax(): Double = maxOf(accountMax(), folderMax()) + + private suspend fun accountMax(): Double = accountDao.findMaxOrderNum() ?: 0.0 + private suspend fun folderMax(): Double = folderDao.findMaxOrderNum() ?: 0.0 +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/WriteAccountsAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/WriteAccountsAct.kt index f0d0ce13ca..71a0fa7a89 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/account/WriteAccountsAct.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/WriteAccountsAct.kt @@ -3,6 +3,7 @@ package com.ivy.core.domain.action.account import com.ivy.core.domain.action.Action import com.ivy.core.domain.action.data.Modify import com.ivy.core.domain.action.transaction.WriteTrnsAct +import com.ivy.core.domain.pure.account.validateAccount import com.ivy.core.domain.pure.mapping.entity.mapToEntity import com.ivy.core.persistence.dao.account.AccountDao import com.ivy.core.persistence.query.TrnQueryExecutor @@ -41,9 +42,15 @@ class WriteAccountsAct @Inject constructor( } private suspend fun save(accounts: List) { - val entities = accounts.map { - mapToEntity(it).copy(sync = SyncState.Syncing) - } + val entities = accounts.filter(::validateAccount) + .map { + it.copy( + name = it.name.trim(), + ) + } + .map { + mapToEntity(it).copy(sync = SyncState.Syncing) + } accountDao.save(entities) } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/AccountFoldersFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/AccountFoldersFlow.kt new file mode 100644 index 0000000000..a23f5fd9f5 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/AccountFoldersFlow.kt @@ -0,0 +1,57 @@ +package com.ivy.core.domain.action.account.folder + +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.account.AccountsFlow +import com.ivy.core.domain.action.data.AccountListItem +import com.ivy.core.domain.action.data.AccountListItem.AccountHolder +import com.ivy.core.domain.action.data.AccountListItem.FolderWithAccounts +import com.ivy.core.persistence.dao.account.AccountFolderDao +import com.ivy.core.persistence.entity.account.AccountFolderEntity +import com.ivy.data.account.Account +import com.ivy.data.account.AccountState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +private typealias FolderId = String + +class AccountFoldersFlow @Inject constructor( + private val accountsFlow: AccountsFlow, + private val accountFolderDao: AccountFolderDao, +) : FlowAction>() { + override fun Unit.createFlow(): Flow> = combine( + accountsFlow(), accountFolderDao.findAll() + ) { accounts, folderEntities -> + val archived = AccountListItem.Archived( + accounts.filter { it.state == AccountState.Archived }.sortedBy { it.orderNum } + ).takeIf { it.accounts.isNotEmpty() } + val notArchived = accounts.filter { it.state == AccountState.Default } + + val foldersMap = notArchived.groupBy { it.folderId?.toString() ?: "none" } + val folders = folderEntities.map { toDomain(foldersMap, it) } + val accountsNotInFolder = foldersMap.filterKeys { accFolderId -> + // accounts with folder "none" aren't in any folder + if (accFolderId == "none") return@filterKeys true + val folderIds = folders.map { it.folder.id } + // the referenced folder by the account doesn't exists if: + !folderIds.contains(accFolderId) + }.values.flatten() + val accountHolders = accountsNotInFolder.map(AccountListItem::AccountHolder) + + val result = if (archived != null) + folders + accountHolders + archived else folders + accountHolders + result.sortedBy { + when (it) { + is AccountHolder -> it.account.orderNum + is FolderWithAccounts -> it.folder.orderNum + is AccountListItem.Archived -> Double.MAX_VALUE - 10 // put archived as last + } + } + } + + private fun toDomain(foldersMap: Map>, entity: AccountFolderEntity) = + FolderWithAccounts( + folder = toDomain(entity), + accounts = foldersMap[entity.id] ?: emptyList(), + ) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/AccountsInFolderAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/AccountsInFolderAct.kt new file mode 100644 index 0000000000..dc2792bbf7 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/AccountsInFolderAct.kt @@ -0,0 +1,18 @@ +package com.ivy.core.domain.action.account.folder + +import com.ivy.common.toUUID +import com.ivy.core.domain.action.Action +import com.ivy.core.domain.action.account.AccountsFlow +import com.ivy.data.account.Account +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import javax.inject.Inject + +class AccountsInFolderAct @Inject constructor( + private val accountsFlow: AccountsFlow, +) : Action>() { + override suspend fun String.willDo(): List { + val folderId = this.toUUID() + return accountsFlow().take(1).first().filter { it.folderId == folderId } + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/FolderAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/FolderAct.kt new file mode 100644 index 0000000000..8fdedc3e05 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/FolderAct.kt @@ -0,0 +1,22 @@ +package com.ivy.core.domain.action.account.folder + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.dao.account.AccountFolderDao +import com.ivy.core.persistence.entity.account.AccountFolderEntity +import com.ivy.data.account.Folder +import javax.inject.Inject + +class FolderAct @Inject constructor( + private val accountFolderDao: AccountFolderDao +) : Action() { + override suspend fun String.willDo(): Folder? = + accountFolderDao.findById(this)?.let(::toDomain) +} + +fun toDomain(entity: AccountFolderEntity) = Folder( + id = entity.id, + name = entity.name, + icon = entity.icon, + color = entity.color, + orderNum = entity.orderNum +) \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/WriteAccountFolderAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/WriteAccountFolderAct.kt new file mode 100644 index 0000000000..354d756a2c --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/WriteAccountFolderAct.kt @@ -0,0 +1,46 @@ +package com.ivy.core.domain.action.account.folder + +import com.ivy.core.domain.action.Action +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.persistence.dao.account.AccountFolderDao +import com.ivy.core.persistence.entity.account.AccountFolderEntity +import com.ivy.data.SyncState +import com.ivy.data.account.Folder +import javax.inject.Inject + +class WriteAccountFolderAct @Inject constructor( + private val accountFolderDao: AccountFolderDao +) : Action, Unit>() { + override suspend fun Modify.willDo() = when (this) { + is Modify.Delete -> delete(this.itemIds) + is Modify.Save -> save(this.items) + } + + private suspend fun delete(folderIds: List) = folderIds.forEach { + accountFolderDao.updateSync(folderId = it, sync = SyncState.Deleting) + } + + private suspend fun save(folders: List) { + accountFolderDao.save( + folders.filter(::validate) + .map { + it.copy(name = it.name.trim()) + } + .map(::toSyncingEntity) + ) + } + + private fun validate(folder: Folder): Boolean { + if (folder.name.isBlank()) return false + return true + } + + private fun toSyncingEntity(domain: Folder) = AccountFolderEntity( + id = domain.id, + name = domain.name, + color = domain.color, + icon = domain.icon, + orderNum = domain.orderNum, + sync = SyncState.Syncing + ) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/WriteAccountFolderContentAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/WriteAccountFolderContentAct.kt new file mode 100644 index 0000000000..43a5b73712 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/account/folder/WriteAccountFolderContentAct.kt @@ -0,0 +1,38 @@ +package com.ivy.core.domain.action.account.folder + +import com.ivy.common.toUUID +import com.ivy.core.domain.action.Action +import com.ivy.core.domain.action.account.AccountsFlow +import com.ivy.core.domain.action.account.WriteAccountsAct +import com.ivy.core.domain.action.account.folder.WriteAccountFolderContentAct.Input +import com.ivy.core.domain.action.data.Modify +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import javax.inject.Inject + +class WriteAccountFolderContentAct @Inject constructor( + private val writeAccountsAct: WriteAccountsAct, + private val accountsFlow: AccountsFlow, +) : Action() { + data class Input( + val folderId: String, + val accountIds: List + ) + + override suspend fun Input.willDo() { + val folderUUID = folderId.toUUID() + val accounts = accountsFlow().take(1).first() + val inFolderOld = accounts.filter { it.folderId == folderUUID } + + // remove accounts no longer in folder + val removeFromFolder = inFolderOld.filter { !accountIds.contains(it.id.toString()) } + .map { it.copy(folderId = null) } + writeAccountsAct(Modify.saveMany(removeFromFolder)) + + // add new accounts to that folder + val addToFolder = accounts.filter { accountIds.contains(it.id.toString()) } + .filter { !inFolderOld.contains(it) } + .map { it.copy(folderId = folderUUID) } + writeAccountsAct(Modify.saveMany(addToFolder)) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/CalculateFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/CalculateFlow.kt index 15b0bf2ca9..3b48ebdcdf 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/CalculateFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/CalculateFlow.kt @@ -96,7 +96,7 @@ class CalculateFlow @Inject constructor( trn: Transaction, arg: SumArg, ): Double = exchange( - ratesData = arg.rates, + exchangeData = arg.rates, from = trn.value.currency, to = arg.outputCurrency, amount = trn.value.amount, diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/account/AccStatsFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/account/AccStatsFlow.kt index a30e590bf8..548e8b5204 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/account/AccStatsFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/account/AccStatsFlow.kt @@ -12,7 +12,7 @@ import com.ivy.data.account.Account import com.ivy.data.time.TimeRange import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject /** @@ -36,7 +36,7 @@ class AccStatsFlow @Inject constructor( @OptIn(FlowPreview::class) override fun Input.createFlow(): Flow = trnsFlow(ByAccountId(account.id) and ActualBetween(range)) - .flatMapMerge { trns -> + .flatMapLatest { trns -> calculateFlow( CalculateFlow.Input( trns = trns, diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/category/CatStatsFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/category/CatStatsFlow.kt index 01a6cab452..53334f76b1 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/category/CatStatsFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/category/CatStatsFlow.kt @@ -12,7 +12,7 @@ import com.ivy.data.category.Category import com.ivy.data.time.TimeRange import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject /** @@ -34,7 +34,7 @@ class CatStatsFlow @Inject constructor( @OptIn(FlowPreview::class) override fun Input.createFlow(): Flow = trnsFlow( ByCategoryId(categoryId = category?.id) and ActualBetween(range) - ).flatMapMerge { trns -> + ).flatMapLatest { trns -> calculateFlow( CalculateFlow.Input( trns = trns, diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/transaction/CollapsedTrnListDatesFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/transaction/CollapsedTrnListDatesFlow.kt new file mode 100644 index 0000000000..203d931424 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/transaction/CollapsedTrnListDatesFlow.kt @@ -0,0 +1,28 @@ +package com.ivy.core.domain.action.calculate.transaction + +import com.ivy.core.domain.action.SharedFlowAction +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +private val collapsedTrnListDates = MutableStateFlow(emptySet()) + +fun toggleTrnListDate(dateId: String) { + collapsedTrnListDates.value = collapsedTrnListDates.value + .toMutableSet() + .apply { + if (dateId in this) { + remove(dateId) + } else { + add(dateId) + } + } +} + +@Singleton +class CollapsedTrnListDatesFlow @Inject constructor() : SharedFlowAction>() { + override fun initialValue(): Set = emptySet() + + override fun createFlow(): Flow> = collapsedTrnListDates +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlow.kt index accd3c30c4..0d78bed2c6 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/transaction/GroupTrnsFlow.kt @@ -1,8 +1,8 @@ package com.ivy.core.domain.action.calculate.transaction -import com.ivy.common.time.TimeProvider +import com.ivy.common.time.dateId +import com.ivy.common.time.provider.TimeProvider import com.ivy.common.time.time -import com.ivy.common.time.timeNow import com.ivy.core.domain.action.FlowAction import com.ivy.core.domain.action.calculate.CalculateFlow import com.ivy.core.domain.pure.calculate.transaction.batchTrns @@ -10,14 +10,13 @@ import com.ivy.core.domain.pure.calculate.transaction.groupActualTrnsByDate import com.ivy.core.domain.pure.transaction.overdue import com.ivy.core.domain.pure.transaction.upcoming import com.ivy.core.domain.pure.util.actualTrns +import com.ivy.core.domain.pure.util.combineSafe import com.ivy.core.domain.pure.util.extractTrns +import com.ivy.core.domain.pure.util.flattenLatest import com.ivy.core.persistence.dao.trn.TrnLinkRecordDao -import com.ivy.data.transaction.DueSection -import com.ivy.data.transaction.Transaction -import com.ivy.data.transaction.TransactionsList -import com.ivy.data.transaction.TrnListItem +import com.ivy.data.transaction.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import java.time.LocalDate import java.time.LocalDateTime @@ -34,17 +33,21 @@ import javax.inject.Inject * * _Note: all calculations are done in base currency._ */ -@OptIn(FlowPreview::class) class GroupTrnsFlow @Inject constructor( private val calculateFlow: CalculateFlow, private val trnLinkRecordDao: TrnLinkRecordDao, private val timeProvider: TimeProvider, + private val collapsedTrnsListDatesFlow: CollapsedTrnListDatesFlow, ) : FlowAction, TransactionsList>() { + @OptIn(ExperimentalCoroutinesApi::class) override fun List.createFlow(): Flow = trnLinkRecordDao.findAll().map { links -> - batchTrns(trns = this, links = links) - }.flatMapMerge { batchedTrnItems -> + val visibleTrns = this.filter { + it.state != TrnState.Hidden + } + batchTrns(trns = visibleTrns, links = links) + }.flatMapLatest { batchedTrnItems -> combine( dueSectionFlow( trnListItems = batchedTrnItems, @@ -68,22 +71,30 @@ class GroupTrnsFlow @Inject constructor( // region Upcoming & Overdue sections private fun dueSectionFlow( trnListItems: List, - dueFilter: (Transaction, now: LocalDateTime) -> Boolean, + dueFilter: (TrnTime, now: LocalDateTime) -> Boolean, ): Flow { - val now = timeNow() - val dueTrns = trnListItems.mapNotNull { + val now = timeProvider.timeNow() + val dueList = trnListItems.filter { when (it) { - is TrnListItem.Trn -> it.trn - else -> null + is TrnListItem.Trn -> dueFilter(it.trn.time, now) + is TrnListItem.Transfer -> dueFilter(it.time, now) + is TrnListItem.DateDivider -> false } - }.filter { dueFilter(it, now) } + } // short circuit & emit null so combine doesn't get stuck - if (dueTrns.isEmpty()) return flowOf(null) + if (dueList.isEmpty()) return flowOf(null) return calculateFlow( CalculateFlow.Input( - trns = dueTrns, + // Calculate Income & Expense only for Trns + due transfer fees + trns = dueList.mapNotNull { + when (it) { + is TrnListItem.Trn -> it.trn + is TrnListItem.Transfer -> it.fee + is TrnListItem.DateDivider -> null + } + }, includeTransfers = false, includeHidden = false, ) @@ -91,7 +102,14 @@ class GroupTrnsFlow @Inject constructor( // the sooner due date, the higher in the list the transaction should appear // upcoming: the most near upcoming trn will appear first // overdue: the most overdue trn will appear first - val sortedTrns = dueTrns.sortedBy { it.time.time() } + val sortedTrns = dueList.sortedBy { + when (it) { + is TrnListItem.Transfer -> it.time.time() + is TrnListItem.Trn -> it.trn.time.time() + // this should never happen because date dividers must be filtered + is TrnListItem.DateDivider -> timeProvider.timeNow() + } + } DueSection( income = dueStats.income, @@ -105,46 +123,54 @@ class GroupTrnsFlow @Inject constructor( // region History private fun historyFlow( trnListItems: List - ): Flow> { - val actualTrns = actualTrns(trnItems = trnListItems) - val trnsByDay = groupActualTrnsByDate( - actualTrns = actualTrns, - timeProvider = timeProvider, - ) - - // emit so the waiting for it "combine" doesn't get stuck - if (trnsByDay.isEmpty()) return flowOf(emptyList()) + ): Flow> = collapsedTrnsListDatesFlow() + .map { collapsedTrnsList -> + val actualTrns = actualTrns(trnItems = trnListItems) + val trnsByDay = groupActualTrnsByDate( + actualTrns = actualTrns, + timeProvider = timeProvider, + ) - // calculate stats for each trn history day - return combine( - trnsByDay.map { (day, trnsForTheDay) -> - trnHistoryDayFlow( - day = day, - trnsForTheDay = trnsForTheDay, - ) + // calculate stats for each trn history day + combineSafe( + flows = trnsByDay.map { (day, trnsForTheDay) -> + trnHistoryDayFlow( + date = day, + trnsForTheDay = trnsForTheDay, + collapsed = collapsedTrnsList.contains(day.dateId()), + ) + }, + ifEmpty = emptyList(), + ) { + it.flatten() } - ) { trnsPerDay -> - trnsPerDay.flatMap { it } - } - } + }.flattenLatest() private fun trnHistoryDayFlow( - day: LocalDate, - trnsForTheDay: List - ): Flow> = calculateFlow( - CalculateFlow.Input( - trns = trnsForTheDay.flatMap(::extractTrns), - outputCurrency = null, - includeTransfers = true, - includeHidden = false, + date: LocalDate, + trnsForTheDay: List, + collapsed: Boolean, + ): Flow> = combine( + calculateFlow( + CalculateFlow.Input( + trns = trnsForTheDay.flatMap(::extractTrns), + outputCurrency = null, + includeTransfers = true, + includeHidden = false, + ), + ), + collapsedTrnsListDatesFlow() + ) { statsForTheDay, collapsedDatesIds -> + val id = date.dateId() + val dateDivider = TrnListItem.DateDivider( + id = id, + date = date, + cashflow = statsForTheDay.balance, + collapsed = id in collapsedDatesIds ) - ).map { statsForTheDay -> - listOf( - TrnListItem.DateDivider( - date = day, - cashflow = statsForTheDay.balance, - ) - ).plus(trnsForTheDay) + + if (collapsed) listOf(dateDivider) else + listOf(dateDivider).plus(trnsForTheDay) } // endregion } \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/wallet/TotalBalanceFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/wallet/TotalBalanceFlow.kt index 12f07ff6b0..5c7bd8edd5 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/calculate/wallet/TotalBalanceFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/calculate/wallet/TotalBalanceFlow.kt @@ -6,6 +6,7 @@ import com.ivy.core.domain.action.calculate.account.AccStatsFlow import com.ivy.core.domain.action.calculate.wallet.TotalBalanceFlow.Input import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow import com.ivy.core.domain.pure.time.allTime +import com.ivy.core.domain.pure.util.combineSafe import com.ivy.data.CurrencyCode import com.ivy.data.Value import kotlinx.coroutines.Dispatchers @@ -23,7 +24,7 @@ class TotalBalanceFlow @Inject constructor( private val accountsFlow: AccountsFlow, private val accStatsFlow: AccStatsFlow, private val baseCurrencyFlow: BaseCurrencyFlow, -) : FlowAction() { +) : FlowAction() { /** * @param withExcludedAccs whether to include excluded accounts in the balance calculation @@ -36,32 +37,32 @@ class TotalBalanceFlow @Inject constructor( override fun Input.createFlow(): Flow = accountsFlow().map { allAccounts -> if (withExcludedAccs) allAccounts else allAccounts.filter { !it.excluded } - }.map { accs -> - outputCurrencyFlow().flatMapMerge { outputCurrency -> - if (accs.isEmpty()) { - return@flatMapMerge flowOf(Value(amount = 0.0, currency = outputCurrency)) - } - combine(accs.map { - accStatsFlow( - AccStatsFlow.Input( - account = it, - range = allTime(), - includeHidden = true, - outputCurrency = outputCurrency, + }.flatMapLatest { accs -> + outputCurrencyFlow().flatMapLatest { outputCurrency -> + combineSafe( + flows = accs.map { + accStatsFlow( + AccStatsFlow.Input( + account = it, + range = allTime(), + includeHidden = true, + outputCurrency = outputCurrency, + ) ) - ) - }) { stats -> - val totalBalance = stats.fold(initial = 0.0) { totalBalance, accStats -> - totalBalance + accStats.balance.amount - } + }, + ifEmpty = Value(amount = 0.0, currency = outputCurrency) + ) { stats -> Value( - amount = totalBalance, + amount = stats.fold( + initial = 0.0 + ) { totalBalance, accStats -> + totalBalance + accStats.balance.amount + }, currency = outputCurrency ) } } - }.flattenMerge() - .flowOn(Dispatchers.Default) + }.flowOn(Dispatchers.Default) private fun Input.outputCurrencyFlow(): Flow = outputCurrency?.let(::flowOf) ?: baseCurrencyFlow() diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoriesFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoriesFlow.kt index 5cdac76794..466d99dd90 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoriesFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoriesFlow.kt @@ -23,18 +23,18 @@ class CategoriesFlow @Inject constructor( override fun createFlow(): Flow> = categoryDao.findAll().map { entities -> - entities.map { toCategory(it) } + entities.map { toDomain(it) } }.flowOn(Dispatchers.Default) +} - private fun toCategory(it: CategoryEntity) = Category( - id = it.id.toUUID(), - name = it.name, - parentCategoryId = it.parentCategoryId?.toUUID(), - color = it.color, - icon = it.icon, - orderNum = it.orderNum, - sync = it.sync, - type = it.type, - state = it.state, - ) -} \ No newline at end of file +fun toDomain(it: CategoryEntity) = Category( + id = it.id.toUUID(), + name = it.name, + parentCategoryId = it.parentCategoryId?.toUUID(), + color = it.color, + icon = it.icon, + orderNum = it.orderNum, + sync = it.sync, + type = it.type, + state = it.state, +) \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoriesListFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoriesListFlow.kt new file mode 100644 index 0000000000..993969e189 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoriesListFlow.kt @@ -0,0 +1,90 @@ +package com.ivy.core.domain.action.category + +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.data.CategoryListItem +import com.ivy.data.category.Category +import com.ivy.data.category.CategoryState +import com.ivy.data.category.CategoryType +import com.ivy.data.transaction.TransactionType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.* +import javax.inject.Inject + +class CategoriesListFlow @Inject constructor( + private val categoriesFlow: CategoriesFlow, +) : FlowAction>() { + /** + * @param trnType - null for all categories + */ + data class Input( + val trnType: TransactionType?, + ) + + override fun Input.createFlow(): Flow> = + categoriesFlow() + // Filter only categories that match the selected transaction type + .map { categories -> + if (trnType != null) { + categories.filter { + when (it.type) { + CategoryType.Income -> trnType == TransactionType.Income + CategoryType.Expense -> trnType == TransactionType.Expense + CategoryType.Both -> true + } + } + } else categories + } + .map { categories -> + val archived = mutableListOf() + val parents = mutableListOf() + val subcategories = mutableMapOf>() + + categories.forEach { + if (it.state == CategoryState.Archived) { + archived.add(it) + return@forEach + } + val parentCategoryId = it.parentCategoryId + if (parentCategoryId == null) { + parents.add(it) + } else { + subcategories.computeIfAbsent(parentCategoryId) { + mutableListOf() + } + subcategories[parentCategoryId]!!.add(it) + } + } + + val notArchived = parents.map { parent -> + val children = subcategories[parent.id]?.takeIf { it.isNotEmpty() } + subcategories.remove(parent.id) + + if (children != null) { + CategoryListItem.ParentCategory( + parent = parent, + children = children.sortedBy { it.orderNum } + ) + } else { + CategoryListItem.CategoryHolder( + parent + ) + } + } + subcategories.values.flatten().map { + CategoryListItem.CategoryHolder(it) + } + + val allItems = if (archived.isNotEmpty()) + notArchived + CategoryListItem.Archived( + archived.sortedBy { it.orderNum } + ) else notArchived + + allItems.sortedBy { + when (it) { + is CategoryListItem.Archived -> Double.MAX_VALUE - 10 + is CategoryListItem.CategoryHolder -> it.category.orderNum + is CategoryListItem.ParentCategory -> it.parent.orderNum + } + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoryByIdAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoryByIdAct.kt new file mode 100644 index 0000000000..85d6987b2e --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/category/CategoryByIdAct.kt @@ -0,0 +1,13 @@ +package com.ivy.core.domain.action.category + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.dao.category.CategoryDao +import com.ivy.data.category.Category +import javax.inject.Inject + +class CategoryByIdAct @Inject constructor( + private val categoryDao: CategoryDao +) : Action() { + override suspend fun String.willDo(): Category? = + categoryDao.findById(this)?.let(::toDomain) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/category/NewCategoryOrderNumAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/category/NewCategoryOrderNumAct.kt new file mode 100644 index 0000000000..9d19e754a9 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/category/NewCategoryOrderNumAct.kt @@ -0,0 +1,12 @@ +package com.ivy.core.domain.action.category + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.dao.category.CategoryDao +import javax.inject.Inject + +class NewCategoryOrderNumAct @Inject constructor( + private val categoryDao: CategoryDao, +) : Action() { + override suspend fun Unit.willDo(): Double = + categoryDao.findMaxNoParentOrderNum()?.plus(1) ?: 0.0 +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/category/WriteCategoriesAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/category/WriteCategoriesAct.kt index 28f58a596a..e5fc2128dd 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/category/WriteCategoriesAct.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/category/WriteCategoriesAct.kt @@ -33,7 +33,10 @@ class WriteCategoriesAct @Inject constructor( private suspend fun save(categories: List) { categoryDao.save( - categories.map { mapToEntity(it).copy(sync = SyncState.Syncing) } + categories + .filter { it.name.isNotBlank() } + .map { it.copy(name = it.name.trim()) } + .map { mapToEntity(it).copy(sync = SyncState.Syncing) } ) } diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/data/AccountListItem.kt b/core/domain/src/main/java/com/ivy/core/domain/action/data/AccountListItem.kt new file mode 100644 index 0000000000..a219bb07dd --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/data/AccountListItem.kt @@ -0,0 +1,14 @@ +package com.ivy.core.domain.action.data + +import com.ivy.data.account.Account +import com.ivy.data.account.Folder + +sealed interface AccountListItem { + data class AccountHolder(val account: Account) : AccountListItem + data class FolderWithAccounts( + val folder: Folder, + val accounts: List, + ) : AccountListItem + + data class Archived(val accounts: List) : AccountListItem +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/data/CategoryListItem.kt b/core/domain/src/main/java/com/ivy/core/domain/action/data/CategoryListItem.kt new file mode 100644 index 0000000000..365ff6c1c7 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/data/CategoryListItem.kt @@ -0,0 +1,18 @@ +package com.ivy.core.domain.action.data + +import com.ivy.data.category.Category + +sealed interface CategoryListItem { + data class CategoryHolder( + val category: Category, + ) : CategoryListItem + + data class ParentCategory( + val parent: Category, + val children: List, + ) : CategoryListItem + + data class Archived( + val categories: List, + ) : CategoryListItem +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeAct.kt new file mode 100644 index 0000000000..aa0b18058a --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeAct.kt @@ -0,0 +1,32 @@ +package com.ivy.core.domain.action.exchange + +import arrow.core.getOrElse +import com.ivy.core.domain.action.Action +import com.ivy.core.domain.pure.exchange.exchange +import com.ivy.data.Value +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +class ExchangeAct @Inject constructor( + private val exchangeRatesFlow: ExchangeRatesFlow, +) : Action() { + data class Input( + val value: Value, + val outputCurrency: String, + ) + + override suspend fun Input.willDo(): Value { + val rates = exchangeRatesFlow().first() + return Value( + amount = exchange( + exchangeData = rates, + from = value.currency, + to = outputCurrency, + amount = value.amount + ).getOrElse { + value.amount // exchange as 1:1 if failed + }, + currency = outputCurrency + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeFlow.kt new file mode 100644 index 0000000000..b5549aadda --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeFlow.kt @@ -0,0 +1,35 @@ +package com.ivy.core.domain.action.exchange + +import arrow.core.getOrElse +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.exchange.ExchangeFlow.Input +import com.ivy.core.domain.pure.exchange.exchange +import com.ivy.data.CurrencyCode +import com.ivy.data.Value +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class ExchangeFlow @Inject constructor( + private val exchangeRatesFlow: ExchangeRatesFlow +) : FlowAction() { + /** + * @param outputCurrency null for baseCurrency + */ + data class Input( + val value: Value, + val outputCurrency: CurrencyCode? = null, + ) + + override fun Input.createFlow(): Flow = + exchangeRatesFlow().map { rates -> + val outputCurrency = this.outputCurrency ?: rates.baseCurrency + val exchangedAmount = exchange( + exchangeData = rates, + from = value.currency, + to = outputCurrency, + amount = value.amount + ).getOrElse { 0.0 } + Value(exchangedAmount, outputCurrency) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeRatesFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeRatesFlow.kt index 277245231f..9e414d990c 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeRatesFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/ExchangeRatesFlow.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import javax.inject.Inject import javax.inject.Singleton @@ -34,7 +34,7 @@ class ExchangeRatesFlow @Inject constructor( @OptIn(FlowPreview::class) override fun createFlow(): Flow = - baseCurrencyFlow().flatMapMerge { baseCurrency -> + baseCurrencyFlow().flatMapLatest { baseCurrency -> combine( exchangeRateDao.findAllByBaseCurrency(baseCurrency), exchangeRateOverrideDao.findAllByBaseCurrency(baseCurrency) diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/exchange/SumValuesInCurrencyFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/SumValuesInCurrencyFlow.kt new file mode 100644 index 0000000000..ec6ef5dbec --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/SumValuesInCurrencyFlow.kt @@ -0,0 +1,41 @@ +package com.ivy.core.domain.action.exchange + +import arrow.core.getOrElse +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.exchange.SumValuesInCurrencyFlow.Input +import com.ivy.core.domain.pure.exchange.exchange +import com.ivy.data.CurrencyCode +import com.ivy.data.Value +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class SumValuesInCurrencyFlow @Inject constructor( + private val exchangeRatesFlow: ExchangeRatesFlow +) : FlowAction() { + /** + * @param outputCurrency null for base currency + */ + data class Input( + val values: List, + val outputCurrency: CurrencyCode? = null, + ) + + override fun Input.createFlow(): Flow = + exchangeRatesFlow().map { rates -> + val outputCurrency = this.outputCurrency ?: rates.baseCurrency + val sum = values.sumOf { + exchange( + exchangeData = rates, + from = it.currency, to = outputCurrency, + amount = it.amount + ).getOrElse { 0.0 } + } + Value( + amount = sum, + currency = outputCurrency + ) + }.flowOn(Dispatchers.Default) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/exchange/SyncExchangeRatesAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/SyncExchangeRatesAct.kt index 96ab295701..8e5e10eb39 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/exchange/SyncExchangeRatesAct.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/exchange/SyncExchangeRatesAct.kt @@ -1,34 +1,35 @@ package com.ivy.core.domain.action.exchange +import com.ivy.common.isNotBlank import com.ivy.core.domain.action.Action -import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow import com.ivy.core.persistence.dao.exchange.ExchangeRateDao import com.ivy.core.persistence.entity.exchange.ExchangeRateEntity import com.ivy.data.CurrencyCode import com.ivy.exchange.RemoteExchangeProvider -import kotlinx.coroutines.flow.first import javax.inject.Inject class SyncExchangeRatesAct @Inject constructor( - private val baseCurrencyFlow: BaseCurrencyFlow, private val exchangeProvider: RemoteExchangeProvider, private val exchangeRateDao: ExchangeRateDao -) : Action() { - override suspend fun Unit.willDo() { - val baseCurrency = baseCurrencyFlow().first() - if (baseCurrency == "") willDo() else syncExchangeRates(baseCurrency) +) : Action() { + override suspend fun String.willDo() { + if (this.isNotBlank()) { + syncExchangeRates(this) + } } private suspend fun syncExchangeRates(baseCurrency: CurrencyCode) { val result = exchangeProvider.fetchExchangeRates(baseCurrency = baseCurrency) exchangeRateDao.save( - result.ratesMap.map { (currency, rate) -> - ExchangeRateEntity( - baseCurrency = baseCurrency, - currency = currency, - rate = rate, - provider = result.provider - ) + result.ratesMap.mapNotNull { (currency, rate) -> + if (rate > 0.0) { + ExchangeRateEntity( + baseCurrency = baseCurrency.uppercase(), + currency = currency.uppercase(), + rate = rate, + provider = result.provider + ) + } else null } ) } diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/helper/TrnsListFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/helper/TrnsListFlow.kt index 964cd68464..81aadb984f 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/helper/TrnsListFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/helper/TrnsListFlow.kt @@ -5,9 +5,10 @@ import com.ivy.core.domain.action.calculate.transaction.GroupTrnsFlow import com.ivy.core.domain.action.transaction.TrnQuery import com.ivy.core.domain.action.transaction.TrnsFlow import com.ivy.data.transaction.TransactionsList +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject class TrnsListFlow @Inject constructor( @@ -15,9 +16,9 @@ class TrnsListFlow @Inject constructor( private val groupTrnsFlow: GroupTrnsFlow ) : FlowAction() { - @OptIn(FlowPreview::class) + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) override fun TrnQuery.createFlow(): Flow = trnsFlow(this) - .flatMapMerge { + .flatMapLatest { groupTrnsFlow(it) } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodFlow.kt index 35ee897186..0e5e761629 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodFlow.kt @@ -1,9 +1,10 @@ package com.ivy.core.domain.action.period +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.domain.action.SharedFlowAction import com.ivy.core.domain.action.settings.startdayofmonth.StartDayOfMonthFlow import com.ivy.core.domain.pure.time.currentMonthlyPeriod -import com.ivy.core.domain.pure.time.dateToSelectedMonthlyPeriod +import com.ivy.core.domain.pure.time.monthlyPeriod import com.ivy.data.time.SelectedPeriod import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -19,15 +20,16 @@ import javax.inject.Singleton class SelectedPeriodFlow @Inject constructor( private val startDayOfMonthFlow: StartDayOfMonthFlow, private val selectedPeriodSignal: SelectedPeriodSignal, + private val timeProvider: TimeProvider, ) : SharedFlowAction() { override fun initialValue(): SelectedPeriod = - currentMonthlyPeriod(startDayOfMonth = 1) + currentMonthlyPeriod(startDayOfMonth = 1, timeProvider = timeProvider) override fun createFlow(): Flow = combine( startDayOfMonthFlow(), selectedPeriodSignal.receive() ) { startDayOfMonth, selectedPeriod -> if (selectedPeriod is SelectedPeriod.Monthly) { - dateToSelectedMonthlyPeriod( + monthlyPeriod( dateInPeriod = selectedPeriod.range.to.minusDays(2).toLocalDate(), startDayOfMonth = startDayOfMonth ) diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodSignal.kt b/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodSignal.kt index efe1004907..f68c423ec2 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodSignal.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/period/SelectedPeriodSignal.kt @@ -1,6 +1,7 @@ package com.ivy.core.domain.action.period import android.content.Context +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.domain.action.SignalFlow import com.ivy.core.domain.pure.time.currentMonthlyPeriod import com.ivy.data.time.SelectedPeriod @@ -12,7 +13,8 @@ import javax.inject.Singleton class SelectedPeriodSignal @Inject constructor( @ApplicationContext private val appContext: Context, + private val timeProvider: TimeProvider ) : SignalFlow() { override fun initialSignal(): SelectedPeriod = - currentMonthlyPeriod(startDayOfMonth = 1) + currentMonthlyPeriod(startDayOfMonth = 1, timeProvider = timeProvider) } \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/settings/applocked/AppLockedFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/settings/applocked/AppLockedFlow.kt new file mode 100644 index 0000000000..70a4e22a22 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/settings/applocked/AppLockedFlow.kt @@ -0,0 +1,17 @@ +package com.ivy.core.domain.action.settings.applocked + +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.persistence.datastore.IvyDataStore +import com.ivy.core.persistence.datastore.keys.SettingsKeys +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class AppLockedFlow @Inject constructor( + private val dataStore: IvyDataStore, + private val settingsKeys: SettingsKeys +) : FlowAction() { + override fun Unit.createFlow(): Flow = + dataStore.get(settingsKeys.appLocked) + .map { it ?: false } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/settings/applocked/WriteAppLockedAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/settings/applocked/WriteAppLockedAct.kt new file mode 100644 index 0000000000..d0bfd28ac5 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/settings/applocked/WriteAppLockedAct.kt @@ -0,0 +1,15 @@ +package com.ivy.core.domain.action.settings.applocked + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.datastore.IvyDataStore +import com.ivy.core.persistence.datastore.keys.SettingsKeys +import javax.inject.Inject + +class WriteAppLockedAct @Inject constructor( + private val dataStore: IvyDataStore, + private val settingsKeys: SettingsKeys +) : Action() { + override suspend fun Boolean.willDo() { + dataStore.put(key = settingsKeys.appLocked, this) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/settings/balance/HideBalanceSettingFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/settings/balance/HideBalanceFlow.kt similarity index 79% rename from core/domain/src/main/java/com/ivy/core/domain/action/settings/balance/HideBalanceSettingFlow.kt rename to core/domain/src/main/java/com/ivy/core/domain/action/settings/balance/HideBalanceFlow.kt index 0939e8e96e..d3f8d9e851 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/settings/balance/HideBalanceSettingFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/settings/balance/HideBalanceFlow.kt @@ -1,15 +1,16 @@ package com.ivy.core.domain.action.settings.balance +import com.ivy.core.domain.action.FlowAction import com.ivy.core.persistence.datastore.IvyDataStore import com.ivy.core.persistence.datastore.keys.SettingsKeys import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject -class HideBalanceSettingFlow @Inject constructor( +class HideBalanceFlow @Inject constructor( private val dataStore: IvyDataStore, private val settingsKeys: SettingsKeys -) : com.ivy.core.domain.action.FlowAction() { +) : FlowAction() { override fun Unit.createFlow(): Flow = dataStore.get(settingsKeys.hideBalance) .map { it ?: false } diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/settings/basecurrency/BaseCurrencyAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/settings/basecurrency/BaseCurrencyAct.kt new file mode 100644 index 0000000000..da15998d54 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/settings/basecurrency/BaseCurrencyAct.kt @@ -0,0 +1,16 @@ +package com.ivy.core.domain.action.settings.basecurrency + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.datastore.IvyDataStore +import com.ivy.core.persistence.datastore.keys.SettingsKeys +import com.ivy.data.CurrencyCode +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject + +class BaseCurrencyAct @Inject constructor( + private val dataStore: IvyDataStore, + private val settingsKeys: SettingsKeys, +) : Action() { + override suspend fun Unit.willDo(): CurrencyCode = + dataStore.get(settingsKeys.baseCurrency).firstOrNull() ?: "" +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/settings/theme/ThemeFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/settings/theme/ThemeFlow.kt new file mode 100644 index 0000000000..a92a9e9648 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/settings/theme/ThemeFlow.kt @@ -0,0 +1,18 @@ +package com.ivy.core.domain.action.settings.theme + +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.persistence.datastore.IvyDataStore +import com.ivy.core.persistence.datastore.keys.SettingsKeys +import com.ivy.data.Theme +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class ThemeFlow @Inject constructor( + private val dataStore: IvyDataStore, + private val settingsKeys: SettingsKeys +) : FlowAction() { + override fun Unit.createFlow(): Flow = + dataStore.get(settingsKeys.theme) + .map { it?.let(Theme::fromCode) ?: Theme.Auto } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/settings/theme/WriteThemeAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/settings/theme/WriteThemeAct.kt new file mode 100644 index 0000000000..51a6f64319 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/settings/theme/WriteThemeAct.kt @@ -0,0 +1,17 @@ +package com.ivy.core.domain.action.settings.theme + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.datastore.IvyDataStore +import com.ivy.core.persistence.datastore.keys.SettingsKeys +import com.ivy.data.Theme +import javax.inject.Inject + +class WriteThemeAct @Inject constructor( + private val dataStore: IvyDataStore, + private val settingsKeys: SettingsKeys +) : Action() { + + override suspend fun Theme.willDo() { + dataStore.put(key = settingsKeys.theme, this.code) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnByIdAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnByIdAct.kt new file mode 100644 index 0000000000..416eebc238 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnByIdAct.kt @@ -0,0 +1,13 @@ +package com.ivy.core.domain.action.transaction + +import com.ivy.core.domain.action.Action +import com.ivy.data.transaction.Transaction +import java.util.* +import javax.inject.Inject + +class TrnByIdAct @Inject constructor( + private val trnsByQueryAct: TrnsByQueryAct, +) : Action() { + override suspend fun UUID.willDo(): Transaction? = + trnsByQueryAct(TrnQuery.ById(this)).firstOrNull() +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnQuery.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnQuery.kt index d3b2174409..51bc503f18 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnQuery.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnQuery.kt @@ -21,6 +21,7 @@ sealed interface TrnQuery { data class ByTypeIn(val types: NonEmptyList) : TrnQuery data class ByPurpose(val purpose: TrnPurpose?) : TrnQuery + data class ByPurposeIn(val purposes: NonEmptyList) : TrnQuery /** * Inclusive period [from, to] @@ -54,6 +55,7 @@ fun TrnQuery.toTrnWhere(): TrnWhere = when (this) { is TrnQuery.ById -> TrnWhere.ById(id.toString()) is TrnQuery.ByIdIn -> TrnWhere.ByIdIn(ids.map { it.toString() }) is TrnQuery.ByPurpose -> TrnWhere.ByPurpose(purpose) + is TrnQuery.ByPurposeIn -> TrnWhere.ByPurposeIn(purposes) is TrnQuery.ByType -> TrnWhere.ByType(trnType) is TrnQuery.ByTypeIn -> TrnWhere.ByTypeIn(types) is TrnQuery.DueBetween -> TrnWhere.DueBetween(range) diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnsByQueryAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnsByQueryAct.kt new file mode 100644 index 0000000000..f28c521ee9 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnsByQueryAct.kt @@ -0,0 +1,14 @@ +package com.ivy.core.domain.action.transaction + +import com.ivy.core.domain.action.Action +import com.ivy.data.transaction.Transaction +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +class TrnsByQueryAct @Inject constructor( + private val trnsFlow: TrnsFlow, +) : Action>() { + override suspend fun TrnQuery.willDo(): List = + trnsFlow(this).first() + +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnsFlow.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnsFlow.kt index 88834c75bb..6d5913521b 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnsFlow.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/TrnsFlow.kt @@ -1,9 +1,13 @@ package com.ivy.core.domain.action.transaction +import com.ivy.common.time.provider.TimeProvider +import com.ivy.common.time.toLocal import com.ivy.common.toUUID import com.ivy.core.domain.action.FlowAction import com.ivy.core.domain.action.account.AccountsFlow import com.ivy.core.domain.action.category.CategoriesFlow +import com.ivy.core.domain.pure.util.combineList +import com.ivy.core.domain.pure.util.flattenLatest import com.ivy.core.persistence.dao.AttachmentDao import com.ivy.core.persistence.dao.tag.TagDao import com.ivy.core.persistence.dao.trn.TrnMetadataDao @@ -13,7 +17,8 @@ import com.ivy.core.persistence.entity.tag.TagEntity import com.ivy.core.persistence.entity.trn.TrnEntity import com.ivy.core.persistence.entity.trn.TrnMetadataEntity import com.ivy.core.persistence.entity.trn.data.TrnTimeType -import com.ivy.core.persistence.query.TrnQueryExecutor +import com.ivy.core.persistence.query.* +import com.ivy.data.SyncState import com.ivy.data.Value import com.ivy.data.account.Account import com.ivy.data.attachment.Attachment @@ -23,14 +28,14 @@ import com.ivy.data.transaction.Transaction import com.ivy.data.transaction.TrnMetadata import com.ivy.data.transaction.TrnTime import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* -import java.time.LocalDateTime -import java.time.ZoneId import java.util.* import javax.inject.Inject /** + * Note: Deleted but not synced transactions aren't returned. * @return a flow of domain **[[Transaction]]** by a given query. * ## Query * @@ -59,43 +64,47 @@ class TrnsFlow @Inject constructor( private val trnTagDao: TrnTagDao, private val tagDao: TagDao, private val trnsSignal: TrnsSignal, + private val timeProvider: TimeProvider, ) : FlowAction>() { - override fun TrnQuery.createFlow(): Flow> = - combine(accountsFlow(), categoriesFlow(), trnsSignal.receive()) { accs, cats, _ -> - val entities = queryExecutor.query(this.toTrnWhere()) - if (entities.isEmpty()) { - return@combine flowOf(emptyList()) - } + override fun TrnQuery.createFlow(): Flow> = combine( + accountsFlow(), categoriesFlow(), trnsSignal.receive() + ) { accs, cats, _ -> + val dbQuery = brackets(this.toTrnWhere()) and not(TrnWhere.BySync(SyncState.Deleting)) + val entities = queryExecutor.query(dbQuery) + if (entities.isEmpty()) { + return@combine flowOf(emptyList()) + } - val accsMap = accs.associateBy { it.id } - val catsMap = cats.associateBy { it.id } + val accsMap = accs.associateBy { it.id } + val catsMap = cats.associateBy { it.id } - combine( - entities.map { - mapTransactionEntityFlow( - accounts = accsMap, - categories = catsMap, - trn = it - ) - } - ) { trns -> - trns.toList() + combineList( + entities.mapNotNull { + mapTransactionEntityFlow( + accounts = accsMap, + categories = catsMap, + trn = it + ) } - }.flattenMerge() - .flowOn(Dispatchers.Default) + ) + }.flattenLatest() + .flowOn(Dispatchers.Default) + @OptIn(ExperimentalCoroutinesApi::class) private fun mapTransactionEntityFlow( accounts: Map, categories: Map, trn: TrnEntity, - ): Flow { - val account = accounts[trn.accountId.toUUID()] ?: return flow {} + ): Flow? { + val account = accounts[trn.accountId.toUUID()] + ?: return null val trnId = trn.id - val tagsFlow = trnTagDao.findByTrnId(trnId = trnId).flatMapMerge { trnTags -> - tagDao.findByTagIds(tagIds = trnTags.map { it.tagId }) - } + val tagsFlow = trnTagDao.findByTrnId(trnId = trnId) + .flatMapLatest { trnTags -> + tagDao.findByTagIds(tagIds = trnTags.map { it.tagId }) + } return combine( trnMetadataDao.findByTrnId(trnId = trnId), @@ -125,8 +134,7 @@ class TrnsFlow @Inject constructor( } private fun trnTime(entity: TrnEntity): TrnTime { - // TODO: Check dateTime conversion correctness - val localeTime = LocalDateTime.ofInstant(entity.time, ZoneId.systemDefault()) + val localeTime = entity.time.toLocal(timeProvider) return when (entity.timeType) { TrnTimeType.Actual -> TrnTime.Actual(localeTime) TrnTimeType.Due -> TrnTime.Due(localeTime) diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/WriteTrnsAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/WriteTrnsAct.kt index 2a9cd8256b..e3c3500d6d 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/WriteTrnsAct.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/WriteTrnsAct.kt @@ -1,9 +1,12 @@ package com.ivy.core.domain.action.transaction +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.domain.action.Action import com.ivy.core.domain.action.data.Modify import com.ivy.core.domain.pure.mapping.entity.mapToEntity import com.ivy.core.domain.pure.mapping.entity.mapToTrnTagEntity +import com.ivy.core.domain.pure.transaction.validateTransaction +import com.ivy.core.domain.pure.util.beautify import com.ivy.core.persistence.dao.AttachmentDao import com.ivy.core.persistence.dao.trn.TrnDao import com.ivy.core.persistence.dao.trn.TrnLinkRecordDao @@ -45,6 +48,7 @@ class WriteTrnsAct @Inject constructor( private val trnLinkRecordDao: TrnLinkRecordDao, private val trnMetadataDao: TrnMetadataDao, private val attachmentDao: AttachmentDao, + private val timeProvider: TimeProvider, ) : Action, Unit>() { override suspend fun Modify.willDo() { @@ -60,7 +64,17 @@ class WriteTrnsAct @Inject constructor( private suspend fun save(trns: List) = trns.forEach { saveTrn(it) } private suspend fun saveTrn(trn: Transaction) { - trnDao.save(mapToEntity(trn).copy(sync = Syncing)) + if (!validateTransaction(trn)) return // don't save invalid transactions + + trnDao.save( + mapToEntity( + trn = trn.copy( + title = beautify(trn.title), + description = beautify(trn.description) + ), + timeProvider = timeProvider, + ).copy(sync = Syncing) + ) // save associated data val trnId = trn.id.toString() diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/ModifyTransfer.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/ModifyTransfer.kt new file mode 100644 index 0000000000..1ac335b989 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/ModifyTransfer.kt @@ -0,0 +1,24 @@ +package com.ivy.core.domain.action.transaction.transfer + +import com.ivy.data.transaction.TrnListItem + +sealed interface ModifyTransfer { + companion object { + fun add(data: TransferData) = Add(data) + + fun edit(batchId: String, data: TransferData) = Edit(batchId, data) + + fun delete(transfer: TrnListItem.Transfer) = Delete(transfer) + } + + data class Add internal constructor( + val data: TransferData + ) : ModifyTransfer + + data class Edit internal constructor( + val batchId: String, + val data: TransferData + ) : ModifyTransfer + + data class Delete internal constructor(val transfer: TrnListItem.Transfer) : ModifyTransfer +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/TransferByBatchIdAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/TransferByBatchIdAct.kt new file mode 100644 index 0000000000..0822469cd4 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/TransferByBatchIdAct.kt @@ -0,0 +1,39 @@ +package com.ivy.core.domain.action.transaction.transfer + +import com.ivy.common.toNonEmptyList +import com.ivy.common.toUUID +import com.ivy.core.domain.action.Action +import com.ivy.core.domain.action.transaction.TrnQuery +import com.ivy.core.domain.action.transaction.TrnsByQueryAct +import com.ivy.core.persistence.dao.trn.TrnLinkRecordDao +import com.ivy.data.transaction.TrnListItem +import com.ivy.data.transaction.TrnPurpose +import javax.inject.Inject + +class TransferByBatchIdAct @Inject constructor( + private val trnsByQueryAct: TrnsByQueryAct, + private val trnLinkRecordDao: TrnLinkRecordDao, +) : Action() { + override suspend fun String.willDo(): TrnListItem.Transfer? { + val trnIds = trnLinkRecordDao.findByBatchId(batchId = this) + .map { it.trnId } + if (trnIds.isEmpty()) return null + val trns = trnsByQueryAct( + TrnQuery.ByIdIn( + trnIds.map { it.toUUID() }.toNonEmptyList() + ) + ) + + val from = trns.firstOrNull { it.purpose == TrnPurpose.TransferFrom } ?: return null + val to = trns.firstOrNull { it.purpose == TrnPurpose.TransferTo } ?: return null + val fee = trns.firstOrNull { it.purpose == TrnPurpose.Fee } + + return TrnListItem.Transfer( + batchId = this, + time = from.time, + from = from, + to = to, + fee = fee, + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/TransferData.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/TransferData.kt new file mode 100644 index 0000000000..12953eca90 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/TransferData.kt @@ -0,0 +1,18 @@ +package com.ivy.core.domain.action.transaction.transfer + +import com.ivy.data.Value +import com.ivy.data.account.Account +import com.ivy.data.category.Category +import com.ivy.data.transaction.TrnTime + +data class TransferData( + val amountFrom: Value, + val amountTo: Value, + val accountFrom: Account, + val accountTo: Account, + val category: Category?, + val time: TrnTime, + val title: String?, + val description: String?, + val fee: Value?, +) \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/WriteTransferAct.kt b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/WriteTransferAct.kt new file mode 100644 index 0000000000..d4836d7c90 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/action/transaction/transfer/WriteTransferAct.kt @@ -0,0 +1,216 @@ +package com.ivy.core.domain.action.transaction.transfer + +import com.ivy.core.domain.action.Action +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.domain.action.transaction.WriteTrnsAct +import com.ivy.core.domain.action.transaction.WriteTrnsBatchAct +import com.ivy.core.domain.pure.transaction.transfer.validateTransfer +import com.ivy.data.SyncState +import com.ivy.data.Value +import com.ivy.data.transaction.* +import java.util.* +import javax.inject.Inject + +class WriteTransferAct @Inject constructor( + private val writeTrnsBatchAct: WriteTrnsBatchAct, + private val transferByBatchIdAct: TransferByBatchIdAct, + private val writeTrnsAct: WriteTrnsAct, +) : Action() { + override suspend fun ModifyTransfer.willDo() { + when (this) { + is ModifyTransfer.Add -> addTransfer(data) + is ModifyTransfer.Edit -> editTransfer(batchId, data) + is ModifyTransfer.Delete -> deleteTransfer(transfer) + } + } + + private suspend fun addTransfer( + data: TransferData + ) { + if (!validateTransfer(data)) return + + val trns = mutableListOf() + val metadata = TrnMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null, + ) + + // FROM + trns.add( + Transaction( + id = UUID.randomUUID(), + account = data.accountFrom, + value = data.amountFrom, + category = data.category, + title = data.title, + description = data.description, + time = data.time, + type = TransactionType.Expense, + purpose = TrnPurpose.TransferFrom, + metadata = metadata, + attachments = emptyList(), + tags = emptyList(), + state = TrnState.Default, + sync = SyncState.Syncing, + ) + ) + + // TO + trns.add( + Transaction( + id = UUID.randomUUID(), + account = data.accountTo, + value = data.amountTo, + category = data.category, + title = data.title, + description = data.description, + time = data.time, + type = TransactionType.Income, + purpose = TrnPurpose.TransferTo, + metadata = metadata, + attachments = emptyList(), + tags = emptyList(), + state = TrnState.Default, + sync = SyncState.Syncing, + ) + ) + + // FEE + if (data.fee != null) { + trns.add( + fee( + data = data, + fee = data.fee, + metadata = metadata, + ) + ) + } + + writeTrnsBatchAct( + WriteTrnsBatchAct.save( + TrnBatch( + batchId = UUID.randomUUID().toString(), + trns = trns + ) + ) + ) + } + + private suspend fun editTransfer( + batchId: String, + data: TransferData + ) { + if (!validateTransfer(data)) return + val transfer = transferByBatchIdAct(batchId) ?: return + + val trns = mutableListOf() + + // FROM + trns.add( + transfer.from.copy( + account = data.accountFrom, + value = data.amountFrom, + category = data.category, + title = data.title, + description = data.description, + time = data.time, + sync = SyncState.Syncing, + ) + ) + + // TO + trns.add( + transfer.to.copy( + account = data.accountTo, + value = data.amountTo, + category = data.category, + title = data.title, + description = data.description, + time = data.time, + sync = SyncState.Syncing, + ) + ) + + // FEE + if (data.fee != null) { + // will have fee + val existingFee = transfer.fee + if (existingFee != null) { + // update existing fee + trns.add( + existingFee.copy( + account = data.accountFrom, + category = data.category, + title = data.title, + description = data.description, + time = data.time, + value = data.fee, + sync = SyncState.Syncing, + ) + ) + } else { + // add new fee + trns.add( + fee( + data = data, + fee = data.fee, + ) + ) + } + } else { + // will have NO fee + transfer.fee?.let { fee -> + // remove existing fee if any + writeTrnsAct(Modify.delete(fee.id.toString())) + } + } + + writeTrnsBatchAct( + WriteTrnsBatchAct.save( + TrnBatch( + batchId = batchId, + trns = trns + ) + ) + ) + } + + private fun fee( + data: TransferData, + fee: Value, + metadata: TrnMetadata = TrnMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null, + ) + ): Transaction = Transaction( + id = UUID.randomUUID(), + account = data.accountFrom, + value = fee, + category = data.category, + title = data.title, + description = data.description, + time = data.time, + type = TransactionType.Expense, + purpose = TrnPurpose.Fee, + metadata = metadata, + attachments = emptyList(), + tags = emptyList(), + state = TrnState.Default, + sync = SyncState.Syncing, + ) + + private suspend fun deleteTransfer( + transfer: TrnListItem.Transfer + ) { + writeTrnsBatchAct( + WriteTrnsBatchAct.delete( + TrnBatch( + batchId = transfer.batchId, + trns = listOfNotNull(transfer.from, transfer.to, transfer.fee) + ) + ) + ) + } +} diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/account/AdjustBalance.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/account/AdjustBalance.kt index d0676b3939..8e05ec53c1 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/account/AdjustBalance.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/account/AdjustBalance.kt @@ -1,6 +1,6 @@ package com.ivy.core.domain.pure.account -import com.ivy.common.time.timeNow +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.domain.pure.isFiat import com.ivy.core.domain.pure.util.isInsignificant import com.ivy.data.SyncState @@ -11,10 +11,11 @@ import java.util.* import kotlin.math.abs fun adjustBalanceTrn( + timeProvider: TimeProvider, account: Account, currentBalance: Double, desiredBalance: Double, - hiddenTrn: Boolean + hiddenTrn: Boolean, ): Transaction? { // if the acc has 50$ and we want to adjust it to 40$ // => we need to create an Expense for $10 @@ -34,7 +35,7 @@ fun adjustBalanceTrn( value = Value(amount = abs(amountMissing), currency = account.currency), title = "Adjust balance", description = null, - time = TrnTime.Actual(timeNow()), + time = TrnTime.Actual(timeProvider.timeNow()), state = if (hiddenTrn) TrnState.Hidden else TrnState.Default, purpose = TrnPurpose.AdjustBalance, diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/account/ValidateAccount.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/account/ValidateAccount.kt new file mode 100644 index 0000000000..091948c903 --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/account/ValidateAccount.kt @@ -0,0 +1,8 @@ +package com.ivy.core.domain.pure.account + +import com.ivy.data.account.Account + +fun validateAccount(account: Account): Boolean { + if (account.name.isBlank()) return false + return true +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/calculate/transaction/GroupTrnsByDate.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/calculate/transaction/GroupTrnsByDate.kt index c02a4b3b60..e705a27b7b 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/calculate/transaction/GroupTrnsByDate.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/calculate/transaction/GroupTrnsByDate.kt @@ -1,6 +1,6 @@ package com.ivy.core.domain.pure.calculate.transaction -import com.ivy.common.time.TimeProvider +import com.ivy.common.time.provider.TimeProvider import com.ivy.common.time.toEpochSeconds import com.ivy.core.domain.pure.util.actualDate import com.ivy.core.domain.pure.util.actualTime diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/dummy/DummyDateDivider.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/dummy/DummyDateDivider.kt index 8cd500f4d6..254659400c 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/dummy/DummyDateDivider.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/dummy/DummyDateDivider.kt @@ -1,4 +1,4 @@ -package com.ivy.core.functions.transaction +package com.ivy.core.domain.pure.dummy import com.ivy.common.time.dateNowUTC import com.ivy.data.Value @@ -6,9 +6,13 @@ import com.ivy.data.transaction.TrnListItem import java.time.LocalDate fun dummyDateDivider( + id: String = "01-01-2023", date: LocalDate = dateNowUTC(), - cashflow: Value = com.ivy.core.domain.pure.dummy.dummyValue() + cashflow: Value = dummyValue(), + collapsed: Boolean = false, ) = TrnListItem.DateDivider( + id = id, date = date, cashflow = cashflow, + collapsed = collapsed, ) \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/exchange/Exchange.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/exchange/Exchange.kt index a1d5ca75dc..8e4fa398e2 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/exchange/Exchange.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/exchange/Exchange.kt @@ -9,7 +9,7 @@ import com.ivy.data.exchange.ExchangeRatesData suspend fun exchange( - ratesData: ExchangeRatesData, + exchangeData: ExchangeRatesData, from: CurrencyCode, to: CurrencyCode, amount: Double, @@ -18,7 +18,7 @@ suspend fun exchange( if (amount == 0.0) return@option 0.0 val rate = findRate( - ratesData = ratesData, + ratesData = exchangeData, from = from, to = to, ).bind() diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/format/CombinedValueUi.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/format/CombinedValueUi.kt new file mode 100644 index 0000000000..4a83d8386b --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/format/CombinedValueUi.kt @@ -0,0 +1,44 @@ +package com.ivy.core.domain.pure.format + +import androidx.compose.runtime.Immutable +import com.ivy.data.Value + +@Immutable +data class CombinedValueUi constructor( + val value: Value, + val valueUi: ValueUi, +) { + companion object { + fun initial() = CombinedValueUi( + value = Value(amount = 0.0, currency = ""), + valueUi = ValueUi(amount = "0.0", currency = ""), + ) + } + + constructor( + amount: Double, + currency: String, + shortenFiat: Boolean, + ) : this( + value = Value(amount, currency), + shortenFiat = shortenFiat, + ) + + constructor( + value: Value, + shortenFiat: Boolean, + ) : this( + value = value, + valueUi = format(value, shortenFiat = shortenFiat), + ) +} + +fun dummyCombinedValueUi( + amount: Double = 0.0, + currency: String = "USD", + shortenFiat: Boolean = false, +) = CombinedValueUi( + amount = amount, + currency = currency, + shortenFiat = shortenFiat, +) \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/format/FormatValue.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/format/FormatValue.kt index b40d8c32ce..2f9c681856 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/format/FormatValue.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/format/FormatValue.kt @@ -12,13 +12,9 @@ fun format( formatCrypto(value) else formatFiat(value = value, shorten = shortenFiat) private fun formatCrypto(value: Value): ValueUi { - tailrec fun removeTrailingZeros(number: String): String = if (number.last() != '0') - number else removeTrailingZeros(number.dropLast(1)) - - val df = DecimalFormat("###,###,##0.${"0".repeat(12)}") - val amountTrailingZeros = df.format(value.amount) + val df = DecimalFormat("###,###,##0.${"#".repeat(16)}") return ValueUi( - amount = removeTrailingZeros(amountTrailingZeros), + amount = df.format(value.amount), currency = value.currency ) } @@ -33,7 +29,7 @@ private fun formatFiat( currency = value.currency ) } else { - val df = DecimalFormat("#,##0.00") + val df = DecimalFormat("###,##0.##") ValueUi( amount = df.format(value.amount), currency = value.currency diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/format/ValueUi.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/format/ValueUi.kt index c6fca92572..6899eb2a82 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/format/ValueUi.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/format/ValueUi.kt @@ -1,12 +1,15 @@ package com.ivy.core.domain.pure.format +import androidx.compose.runtime.Immutable + +@Immutable data class ValueUi( val amount: String, val currency: String, ) fun dummyValueUi( - amount: String = "0.0", + amount: String = "0", currency: String = "USD" ) = ValueUi( amount = amount, currency = currency, diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/mapping/entity/TrnEntityMapping.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/mapping/entity/TrnEntityMapping.kt index 1bb924a53f..6f19019696 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/mapping/entity/TrnEntityMapping.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/mapping/entity/TrnEntityMapping.kt @@ -1,13 +1,17 @@ package com.ivy.core.domain.pure.mapping.entity +import com.ivy.common.time.provider.TimeProvider import com.ivy.common.time.time +import com.ivy.common.time.toUtc import com.ivy.core.persistence.entity.trn.TrnEntity import com.ivy.core.persistence.entity.trn.data.TrnTimeType import com.ivy.data.transaction.Transaction import com.ivy.data.transaction.TrnTime -import java.time.ZoneOffset -fun mapToEntity(trn: Transaction) = with(trn) { +fun mapToEntity( + trn: Transaction, + timeProvider: TimeProvider, +) = with(trn) { TrnEntity( id = id.toString(), accountId = account.id.toString(), @@ -20,8 +24,7 @@ fun mapToEntity(trn: Transaction) = with(trn) { categoryId = category?.id?.toString(), title = title, description = description, - // TODO: Check time conversion correctness - time = time.time().toInstant(ZoneOffset.UTC), + time = time.time().toUtc(timeProvider), timeType = if (time is TrnTime.Actual) TrnTimeType.Actual else TrnTimeType.Due, ) } \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/time/DynamicTimePeriod.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/time/DynamicTimePeriod.kt index 731be0c83d..faf5049cd4 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/time/DynamicTimePeriod.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/time/DynamicTimePeriod.kt @@ -1,22 +1,86 @@ package com.ivy.core.domain.pure.time +import com.ivy.common.time.* +import com.ivy.common.time.provider.TimeProvider import com.ivy.data.time.DynamicTimePeriod import com.ivy.data.time.TimeRange import com.ivy.data.time.TimeUnit -// region Calendar -fun DynamicTimePeriod.toRange(startDayOfMonth: Int): TimeRange = when (this) { - is DynamicTimePeriod.Calendar -> TODO() - is DynamicTimePeriod.Last -> TODO() +fun DynamicTimePeriod.toRange( + startDayOfMonth: Int, + timeProvider: TimeProvider +): TimeRange = when (this) { + is DynamicTimePeriod.Calendar -> toRange(startDayOfMonth, timeProvider) + is DynamicTimePeriod.Last -> toRange(timeProvider) is DynamicTimePeriod.Next -> TODO() } +// region Calendar fun DynamicTimePeriod.Calendar.toRange( - startDayOfMonth: Int -): TimeRange = when (unit) { - TimeUnit.Day -> TODO() - TimeUnit.Week -> TODO() - TimeUnit.Month -> TODO() - TimeUnit.Year -> TODO() + startDayOfMonth: Int, + timeProvider: TimeProvider, +): TimeRange { + val today = timeProvider.dateNow() + val offset = offset.toLong() + return when (unit) { + TimeUnit.Day -> today.plusDays(offset) + .let { + TimeRange( + from = it.atStartOfDay(), + to = it.atEndOfDay() + ) + } + TimeUnit.Week -> today.plusWeeks(offset).let { + TimeRange( + from = startOfWeek(it).atStartOfDay(), + to = endOfWeek(it).atEndOfDay() + ) + } + TimeUnit.Month -> monthlyTimeRange( + date = today.plusMonths(offset), startDayOfMonth = startDayOfMonth + ) + TimeUnit.Year -> today.plusYears(offset).let { + TimeRange( + from = startOfYear(it).atStartOfDay(), + to = endOfYear(it).atEndOfDay() + ) + } + } +} +// endregion + +// region Last +fun DynamicTimePeriod.Last.toRange( + timeProvider: TimeProvider, +): TimeRange { + val today = timeProvider.dateNow() + val adjustedN = n.toLong() - 1 // because it includes today + return TimeRange( + from = when (unit) { + TimeUnit.Day -> today.minusDays(adjustedN) + TimeUnit.Week -> today.minusWeeks(adjustedN) + TimeUnit.Month -> today.minusMonths(adjustedN) + TimeUnit.Year -> today.minusYears(adjustedN) + }.atStartOfDay(), + to = today.atEndOfDay() + ) +} +// endregion + +// region Next +fun DynamicTimePeriod.Next.toRange( + timeProvider: TimeProvider, +): TimeRange { + val today = timeProvider.dateNow() + val adjustedN = n.toLong() - 1 // because it includes today + return TimeRange( + from = today.atStartOfDay(), + to = when (unit) { + TimeUnit.Day -> today.plusDays(adjustedN) + TimeUnit.Week -> today.plusWeeks(adjustedN) + TimeUnit.Month -> today.plusMonths(adjustedN) + TimeUnit.Year -> today.plusYears(adjustedN) + }.atStartOfDay() + ) } // endregion \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/time/MonthlyPeriod.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/time/MonthlyPeriod.kt index 1c216cc43f..e5082a1245 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/time/MonthlyPeriod.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/time/MonthlyPeriod.kt @@ -1,39 +1,44 @@ package com.ivy.core.domain.pure.time -import com.ivy.common.time.* +import com.ivy.common.time.atEndOfDay +import com.ivy.common.time.endOfMonth +import com.ivy.common.time.provider.TimeProvider +import com.ivy.common.time.startOfMonth +import com.ivy.common.time.withDayOfMonthSafe import com.ivy.data.time.Month import com.ivy.data.time.SelectedPeriod import com.ivy.data.time.TimeRange import java.time.LocalDate -// TODO: Refactor this file cuz it's bad... - -// TODO: Fix edge-cases when re-working time +// region Current monthly period fun currentMonthlyPeriod( - startDayOfMonth: Int + startDayOfMonth: Int, + timeProvider: TimeProvider ): SelectedPeriod { - val dateNowUTC = dateNowUTC() - val dayToday = dateNowUTC.dayOfMonth + val today = timeProvider.dateNow() - //Examples month = Nov. startDate = 7; Period = from Nov (7) till Dec (6) - // => new period starts if today => startDayOfMonth - val newPeriodStarted = dayToday >= startDayOfMonth + //Example: today = Nov (7), startDate = 7; + // Current period = from Nov (7) till Dec (6) + // => new period starts ony if "today => startDayOfMonth" + val newPeriodStarted = today.dayOfMonth >= startDayOfMonth val periodDate = if (newPeriodStarted) { - //new monthly period has already started then observe it => current month - dateNowUTC + // new monthly period has already started then observe it => current month + today } else { - //new monthly period hasn't yet started then observe the ongoing one => previous month - dateNowUTC.minusMonths(1) + // new monthly period hasn't yet started then observe the ongoing one => previous month + today.minusMonths(1) } - return dateToSelectedMonthlyPeriod( + return monthlyPeriod( dateInPeriod = periodDate, startDayOfMonth = startDayOfMonth, ) } +// endregion -fun dateToSelectedMonthlyPeriod( +// region Monthly period from date +fun monthlyPeriod( dateInPeriod: LocalDate, startDayOfMonth: Int, ): SelectedPeriod.Monthly = SelectedPeriod.Monthly( @@ -42,34 +47,27 @@ fun dateToSelectedMonthlyPeriod( year = dateInPeriod.year, ), startDayOfMonth = startDayOfMonth, - range = dateToMonthlyPeriod(date = dateInPeriod, startDayOfMonth = startDayOfMonth), + range = monthlyTimeRange(date = dateInPeriod, startDayOfMonth = startDayOfMonth), ) -private fun dateToMonthlyPeriod(date: LocalDate, startDayOfMonth: Int): TimeRange = +fun monthlyTimeRange(date: LocalDate, startDayOfMonth: Int): TimeRange = if (startDayOfMonth != 1) { - customStartDayOfMonthPeriodRange( - date = date, - startDateOfMonth = startDayOfMonth - ) - } else { - TimeRange(from = startOfMonth(date), to = endOfMonth(date)) - } + val from = date + .withDayOfMonthSafe(startDayOfMonth) + .atStartOfDay() -private fun customStartDayOfMonthPeriodRange( - date: LocalDate, - startDateOfMonth: Int -): TimeRange { - val from = date - .withDayOfMonthSafe(startDateOfMonth) - .atStartOfDay() + val to = date + .plusMonths(1) + .withDayOfMonthSafe(startDayOfMonth) + //e.g. Correct: 14.10-13.11 (Incorrect: 14.10-14.11) + .minusDays(1) + .atEndOfDay() - val to = date - //startDayOfMonth != 1 just shift N day the month forward so to should +1 month - .plusMonths(1) - .withDayOfMonthSafe(startDateOfMonth) - //e.g. Correct: 14.10-13.11 (Incorrect: 14.10-14.11) - .minusDays(1) - .atEndOfDay() - - return TimeRange(from = from, to = to) -} \ No newline at end of file + TimeRange(from = from, to = to) + } else { + TimeRange( + from = startOfMonth(date).atStartOfDay(), + to = endOfMonth(date).atEndOfDay() + ) + } +// endregion \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/time/PeriodFunctions.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/time/PeriodFunctions.kt index b7b9120d20..ee797b7c24 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/time/PeriodFunctions.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/time/PeriodFunctions.kt @@ -32,7 +32,7 @@ fun periodLengthDays(range: TimeRange): Int { return daysLong.toInt() } -fun yearPeriod(year: Int): SelectedPeriod.CustomRange = SelectedPeriod.CustomRange( +fun yearlyPeriod(year: Int): SelectedPeriod.CustomRange = SelectedPeriod.CustomRange( range = TimeRange( from = LocalDate.of(year, 1, 1).atStartOfDay(), to = LocalDate.of(year, 12, 31).atEndOfDay() diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/SumTransactions.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/SumTransactions.kt index bc07006d97..887995e728 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/SumTransactions.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/SumTransactions.kt @@ -47,7 +47,6 @@ import com.ivy.data.transaction.Transaction * println("Expense = $res[1]") * ``` */ - suspend fun sumTransactions( transactions: List, selectors: NonEmptyList Double>, diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/TrnFilters.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/TrnFilters.kt index 94f019877c..3037b6c598 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/TrnFilters.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/TrnFilters.kt @@ -4,10 +4,10 @@ import com.ivy.data.transaction.Transaction import com.ivy.data.transaction.TrnTime import java.time.LocalDateTime -fun upcoming(trn: Transaction, timeNow: LocalDateTime): Boolean = - (trn.time as? TrnTime.Due)?.due?.isAfter(timeNow.plusSeconds(1)) ?: false +fun upcoming(time: TrnTime, timeNow: LocalDateTime): Boolean = + (time as? TrnTime.Due)?.due?.isAfter(timeNow.plusSeconds(1)) ?: false -fun overdue(trn: Transaction, timeNow: LocalDateTime): Boolean = - (trn.time as? TrnTime.Due)?.due?.isBefore(timeNow) ?: false +fun overdue(time: TrnTime, timeNow: LocalDateTime): Boolean = + (time as? TrnTime.Due)?.due?.isBefore(timeNow) ?: false fun actual(trn: Transaction): Boolean = trn.time is TrnTime.Actual \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/ValidateTransaction.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/ValidateTransaction.kt new file mode 100644 index 0000000000..31e093d4ef --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/ValidateTransaction.kt @@ -0,0 +1,8 @@ +package com.ivy.core.domain.pure.transaction + +import com.ivy.data.transaction.Transaction + +fun validateTransaction(trn: Transaction): Boolean { + if (trn.value.amount <= 0) return false + return true +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/transfer/ValidateTransfer.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/transfer/ValidateTransfer.kt new file mode 100644 index 0000000000..f552bb722a --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/transaction/transfer/ValidateTransfer.kt @@ -0,0 +1,11 @@ +package com.ivy.core.domain.pure.transaction.transfer + +import com.ivy.core.domain.action.transaction.transfer.TransferData + +fun validateTransfer(data: TransferData): Boolean { + if (data.accountFrom == data.accountTo) return false + if (data.amountFrom.amount <= 0) return false + if (data.amountTo.amount <= 0) return false + if (data.fee != null && data.fee.amount <= 0) return false + return true +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/ui/GroupByRows.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/ui/GroupByRows.kt index 80e2972a90..dc0af7b8b6 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/ui/GroupByRows.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/ui/GroupByRows.kt @@ -2,16 +2,14 @@ package com.ivy.core.domain.pure.ui fun groupByRows( items: List, - iconsPerRow: Int, + itemsPerRow: Int, ): List> { val rows = mutableListOf>() var row = mutableListOf() for (icon in items) { - if (row.size < iconsPerRow) { - // row not finished - row.add(icon) - } else { - // row is finished, add it and start the next row + row.add(icon) + if (row.size == itemsPerRow) { + // row finished => add it and start a new row rows.add(row) // row.clear() won't work because it clears the already added row row = mutableListOf() diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/util/FlowUtil.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/util/FlowUtil.kt new file mode 100644 index 0000000000..ed45583ebe --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/util/FlowUtil.kt @@ -0,0 +1,380 @@ +package com.ivy.core.domain.pure.util + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +/** + * @return list of flows -> flow of list + */ +inline fun combineList(flows: List>): Flow> = + if (flows.isEmpty()) flowOf(emptyList()) else combine(flows, Array::toList) + +inline fun combineSafe( + flows: List>, + ifEmpty: R, + crossinline transform: suspend (List) -> R, +): Flow = if (flows.isEmpty()) flowOf(ifEmpty) else + combine(flows) { res -> transform(res.toList()) } + +@OptIn(ExperimentalCoroutinesApi::class) +inline fun Flow>.flattenLatest(): Flow = + flatMapLatest { it } + + +// region combine (more than 5) +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6) -> R +): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R +): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R +): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + flow11: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10, flow11 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + args[10] as T11, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + flow11: Flow, + flow12: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10, flow11, flow12 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + args[10] as T11, + args[11] as T12, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + flow11: Flow, + flow12: Flow, + flow13: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, + flow7, flow8, flow9, flow10, flow11, flow12, flow13 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + args[10] as T11, + args[11] as T12, + args[12] as T13, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + flow11: Flow, + flow12: Flow, + flow13: Flow, + flow14: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, + flow7, flow8, flow9, flow10, flow11, flow12, flow13, flow14 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + args[10] as T11, + args[11] as T12, + args[12] as T13, + args[13] as T14, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + flow11: Flow, + flow12: Flow, + flow13: Flow, + flow14: Flow, + flow15: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, + flow7, flow8, flow9, flow10, flow11, flow12, + flow13, flow14, flow15 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + args[10] as T11, + args[11] as T12, + args[12] as T13, + args[13] as T14, + args[14] as T15, + ) +} + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + flow7: Flow, + flow8: Flow, + flow9: Flow, + flow10: Flow, + flow11: Flow, + flow12: Flow, + flow13: Flow, + flow14: Flow, + flow15: Flow, + flow16: Flow, + transform: suspend ( + T1, T2, T3, T4, T5, + T6, T7, T8, T9, T10, + T11, T12, T13, T14, T15, T16 + ) -> R +): Flow = combine( + flow, flow2, flow3, flow4, flow5, flow6, + flow7, flow8, flow9, flow10, flow11, flow12, + flow13, flow14, flow15, flow16 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10, + args[10] as T11, + args[11] as T12, + args[12] as T13, + args[13] as T14, + args[14] as T15, + args[15] as T16, + ) +} +// endregion \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/util/NumberUtil.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/util/NumberUtil.kt index b4f943f492..aaacd04429 100644 --- a/core/domain/src/main/java/com/ivy/core/domain/pure/util/NumberUtil.kt +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/util/NumberUtil.kt @@ -47,7 +47,7 @@ fun formatShortened(number: Double): String { fun formatShortened(shortened: Double, magnitude: String): String { val decimalPart = split(shortened).decimalPart return if (isSignificant(decimalPart)) { - val df = DecimalFormat("###,##0.00") + val df = DecimalFormat("###,##0.##") "${df.format(shortened)}$magnitude" } else { "${shortened.roundToInt()}$magnitude" @@ -61,7 +61,7 @@ fun formatShortened(number: Double): String { abs(number) >= 1_000 -> { formatShortened(number / 1_000, "k") } - else -> DecimalFormat("0.00").format(number) + else -> DecimalFormat("0.##").format(number) } } // endregion \ No newline at end of file diff --git a/core/domain/src/main/java/com/ivy/core/domain/pure/util/TextUtil.kt b/core/domain/src/main/java/com/ivy/core/domain/pure/util/TextUtil.kt new file mode 100644 index 0000000000..e46c3d9c7d --- /dev/null +++ b/core/domain/src/main/java/com/ivy/core/domain/pure/util/TextUtil.kt @@ -0,0 +1,6 @@ +package com.ivy.core.domain.pure.util + +fun beautify(text: String?): String? = + text?.trim()?.takeIf { it.isNotBlank() } + +fun String?.takeIfNotBlank(): String? = this?.takeIf { it.isNotBlank() } \ No newline at end of file diff --git a/core/domain/src/test/java/com/ivy/core/domain/pure/account/AdjustBalanceTest.kt b/core/domain/src/test/java/com/ivy/core/domain/pure/account/AdjustBalanceTest.kt index 353a113d6c..5419f27559 100644 --- a/core/domain/src/test/java/com/ivy/core/domain/pure/account/AdjustBalanceTest.kt +++ b/core/domain/src/test/java/com/ivy/core/domain/pure/account/AdjustBalanceTest.kt @@ -1,5 +1,6 @@ package com.ivy.core.domain.pure.account +import com.ivy.common.test.testTimeProvider import com.ivy.core.domain.pure.dummy.dummyAcc import com.ivy.data.Value import com.ivy.data.account.Account @@ -42,6 +43,7 @@ class AdjustBalanceTest : StringSpec({ val acc = dummyAcc(currency = "USD") val adjustTrn = adjustBalanceTrn( + timeProvider = testTimeProvider(), account = acc, currentBalance = 50.0, desiredBalance = 40.0, @@ -61,6 +63,7 @@ class AdjustBalanceTest : StringSpec({ val acc = dummyAcc(currency = "EUR") val adjustTrn = adjustBalanceTrn( + timeProvider = testTimeProvider(), account = acc, currentBalance = 33.67, desiredBalance = 100.0, @@ -79,6 +82,7 @@ class AdjustBalanceTest : StringSpec({ val acc = dummyAcc(currency = "USD") val res = adjustBalanceTrn( + timeProvider = testTimeProvider(), account = acc, currentBalance = 1_023.55, desiredBalance = 1_023.555, @@ -92,6 +96,7 @@ class AdjustBalanceTest : StringSpec({ val acc = dummyAcc(currency = "BTC") val res = adjustBalanceTrn( + timeProvider = testTimeProvider(), account = acc, currentBalance = .00345, desiredBalance = .00346, diff --git a/core/domain/src/test/java/com/ivy/core/domain/pure/format/FormatValueTest.kt b/core/domain/src/test/java/com/ivy/core/domain/pure/format/FormatValueTest.kt index 53363cd965..1b2d07a725 100644 --- a/core/domain/src/test/java/com/ivy/core/domain/pure/format/FormatValueTest.kt +++ b/core/domain/src/test/java/com/ivy/core/domain/pure/format/FormatValueTest.kt @@ -8,7 +8,7 @@ import io.kotest.property.arbitrary.boolean import io.kotest.property.checkAll class FormatValueTest : StringSpec({ - "format 5 USD into 5.00 USD" { + "format 5 USD into 5 USD" { checkAll(Arb.boolean()) { shorten -> val res = format( value = Value(5.0, "USD"), @@ -16,7 +16,7 @@ class FormatValueTest : StringSpec({ ) res shouldBe ValueUi( - amount = "5.00", + amount = "5", currency = "USD" ) } diff --git a/core/domain/src/test/java/com/ivy/core/domain/pure/time/DynamicTimePeriodTest.kt b/core/domain/src/test/java/com/ivy/core/domain/pure/time/DynamicTimePeriodTest.kt new file mode 100644 index 0000000000..bf14b1f7f0 --- /dev/null +++ b/core/domain/src/test/java/com/ivy/core/domain/pure/time/DynamicTimePeriodTest.kt @@ -0,0 +1,120 @@ +package com.ivy.core.domain.pure.time + +import com.ivy.common.test.testTimeProvider +import com.ivy.common.time.provider.TimeProvider +import com.ivy.data.time.DynamicTimePeriod +import com.ivy.data.time.TimeRange +import com.ivy.data.time.TimeUnit +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.localTime +import io.kotest.property.arbitrary.next +import java.time.LocalDate +import java.time.LocalDateTime + +class DynamicTimePeriodTest : StringSpec({ + fun timeProvider(date: LocalDate): TimeProvider = testTimeProvider( + date.atTime(Arb.localTime().next()) + ) + + // region Convert Calendar period + "converts daily dynamic period to range" { + val offset = Arb.int(-10, 10).next() + val dynamic = DynamicTimePeriod.Calendar(unit = TimeUnit.Day, offset = offset) + val timeProvider = timeProvider(LocalDate.of(2022, 10, 5)) + + val res = dynamic.toRange(startDayOfMonth = 1, timeProvider = timeProvider) + + res shouldBe TimeRange( + from = LocalDateTime.of( + 2022, 10, 5, + 0, 0, 0 + ).plusDays(offset.toLong()), + to = LocalDateTime.of( + 2022, 10, 5, + 23, 59, 59 + ).plusDays(offset.toLong()) + ) + } + + "converts weekly dynamic period to range" { + val offset = Arb.int(-10, 10).next() + val dynamic = DynamicTimePeriod.Calendar(unit = TimeUnit.Week, offset = offset) + val timeProvider = timeProvider(LocalDate.of(2022, 10, 5)) + + val res = dynamic.toRange(startDayOfMonth = 1, timeProvider = timeProvider) + + res shouldBe TimeRange( + from = LocalDateTime.of( + 2022, 10, 3, + 0, 0, 0 + ).plusWeeks(offset.toLong()), + to = LocalDateTime.of( + 2022, 10, 9, + 23, 59, 59 + ).plusWeeks(offset.toLong()) + ) + } + + "converts monthly dynamic period, start day of month 1" { + val dynamic = DynamicTimePeriod.Calendar(unit = TimeUnit.Month, offset = 0) + val timeProvider = timeProvider(LocalDate.of(2022, 10, 5)) + + val res = dynamic.toRange(startDayOfMonth = 1, timeProvider = timeProvider) + + res shouldBe TimeRange( + from = LocalDateTime.of( + 2022, 10, 1, + 0, 0, 0 + ), + to = LocalDateTime.of( + 2022, 10, 31, + 23, 59, 59 + ) + ) + } + + "converts yearly dynamic period to range" { + val offset = Arb.int(-10, 10).next() + val dynamic = DynamicTimePeriod.Calendar(unit = TimeUnit.Year, offset = offset) + val timeProvider = timeProvider(LocalDate.of(2022, 10, 5)) + + val res = dynamic.toRange(startDayOfMonth = 1, timeProvider = timeProvider) + + res shouldBe TimeRange( + from = LocalDateTime.of( + 2022, 1, 1, + 0, 0, 0 + ).plusYears(offset.toLong()), + to = LocalDateTime.of( + 2022, 12, 31, + 23, 59, 59 + ).plusYears(offset.toLong()) + ) + } + // endregion + + // Convert "Last" period + "converts 'last 3 days' period to range" { + val dynamic = DynamicTimePeriod.Last(n = 3, unit = TimeUnit.Day) + val timeProvider = timeProvider(LocalDate.of(2022, 10, 5)) + + val res = dynamic.toRange(startDayOfMonth = 1, timeProvider = timeProvider) + + res shouldBe TimeRange( + from = LocalDateTime.of( + 2022, 10, 3, + 0, 0, 0 + ), + to = LocalDateTime.of( + 2022, 10, 5, + 23, 59, 59 + ) + ) + } + // endregion + + // TODO: Cover all cases with tests +}) \ No newline at end of file diff --git a/core/domain/src/test/java/com/ivy/core/domain/pure/ui/GroupByRowsTest.kt b/core/domain/src/test/java/com/ivy/core/domain/pure/ui/GroupByRowsTest.kt new file mode 100644 index 0000000000..b82b8dcb8b --- /dev/null +++ b/core/domain/src/test/java/com/ivy/core/domain/pure/ui/GroupByRowsTest.kt @@ -0,0 +1,50 @@ +package com.ivy.core.domain.pure.ui + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class GroupByRowsTest : FreeSpec({ + "grouping by rows" - { + withData( + nameFn = { (items, itemsPerRow, expected) -> + "$items - $itemsPerRow per row = $expected" + }, + // Dataset (by) Items per row (results in) Grouped + row( + listOf(1, 2, 3, 4, 5, 6, 7), 3, listOf( + listOf(1, 2, 3), + listOf(4, 5, 6), + listOf(7), + ) + ), + row( + listOf(1, 2, 3), 1, listOf( + listOf(1), + listOf(2), + listOf(3), + ) + ), + row(listOf(), 4, listOf()), + row( + (1..12).toList(), 4, listOf( + listOf(1, 2, 3, 4), + listOf(5, 6, 7, 8), + listOf(9, 10, 11, 12), + ) + ), + row( + (1..12).toList(), 5, listOf( + listOf(1, 2, 3, 4, 5), + listOf(6, 7, 8, 9, 10), + listOf(11, 12), + ) + ), + ) { (items, itemsPerRow, expected) -> + val res = groupByRows(items = items, itemsPerRow = itemsPerRow) + + res shouldBe expected + } + } +}) \ No newline at end of file diff --git a/core/domain/src/test/java/com/ivy/core/domain/pure/util/ShortenNumberTest.kt b/core/domain/src/test/java/com/ivy/core/domain/pure/util/ShortenNumberTest.kt index d9a03d00e3..967aa4a0c5 100644 --- a/core/domain/src/test/java/com/ivy/core/domain/pure/util/ShortenNumberTest.kt +++ b/core/domain/src/test/java/com/ivy/core/domain/pure/util/ShortenNumberTest.kt @@ -16,10 +16,10 @@ class ShortenNumberTest : StringSpec({ res shouldBe "23.51k" } - "shorten 999 into 999.00" { + "shorten 999 into 999" { val res = formatShortened(999.0) - res shouldBe "999.00" + res shouldBe "999" } "shorten 10,000,000.90 into 10m" { diff --git a/core/exchange-provider/src/androidTest/java/com/ivy/exchange/coinbase/CoinbaseExchangeProviderTest.kt b/core/exchange-provider/src/androidTest/java/com/ivy/exchange/provider/Fawazahmed0ExchangeProviderTest.kt similarity index 76% rename from core/exchange-provider/src/androidTest/java/com/ivy/exchange/coinbase/CoinbaseExchangeProviderTest.kt rename to core/exchange-provider/src/androidTest/java/com/ivy/exchange/provider/Fawazahmed0ExchangeProviderTest.kt index 4c11bf450d..8f8a1ffd08 100644 --- a/core/exchange-provider/src/androidTest/java/com/ivy/exchange/coinbase/CoinbaseExchangeProviderTest.kt +++ b/core/exchange-provider/src/androidTest/java/com/ivy/exchange/provider/Fawazahmed0ExchangeProviderTest.kt @@ -1,7 +1,8 @@ -package com.ivy.exchange.coinbase +package com.ivy.exchange.provider import com.ivy.common.androidtest.AndroidTest import com.ivy.data.exchange.ExchangeProvider +import com.ivy.exchange.fawazahmed0.Fawazahmed0ExchangeProvider import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import io.kotest.matchers.ints.shouldBeGreaterThan @@ -14,14 +15,13 @@ import javax.inject.Inject @AndroidTest @HiltAndroidTest -class CoinbaseExchangeProviderTest { +class Fawazahmed0ExchangeProviderTest { @get:Rule var hiltRule = HiltAndroidRule(this) - @Inject - lateinit var sut: CoinbaseExchangeProvider + lateinit var sut: Fawazahmed0ExchangeProvider @Before fun setUp() { @@ -32,7 +32,7 @@ class CoinbaseExchangeProviderTest { fun fetch_exchange_rates_successfully(): Unit = runBlocking { val res = sut.fetchExchangeRates(baseCurrency = "USD") - res.provider shouldBe ExchangeProvider.Coinbase + res.provider shouldBe ExchangeProvider.Fawazahmed0 res.ratesMap.size shouldBeGreaterThan 1 println("rates: ${res.ratesMap}") } diff --git a/core/exchange-provider/src/main/AndroidManifest.xml b/core/exchange-provider/src/main/AndroidManifest.xml index 81aad16a50..6bf9c7834d 100644 --- a/core/exchange-provider/src/main/AndroidManifest.xml +++ b/core/exchange-provider/src/main/AndroidManifest.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/core/exchange-provider/src/main/java/com/ivy/exchange/RemoteExchangeProvider.kt b/core/exchange-provider/src/main/java/com/ivy/exchange/RemoteExchangeProvider.kt index 2f02e346de..8e69d9a1cf 100644 --- a/core/exchange-provider/src/main/java/com/ivy/exchange/RemoteExchangeProvider.kt +++ b/core/exchange-provider/src/main/java/com/ivy/exchange/RemoteExchangeProvider.kt @@ -7,6 +7,9 @@ import com.ivy.data.exchange.ExchangeProvider interface RemoteExchangeProvider { suspend fun fetchExchangeRates(baseCurrency: CurrencyCode): Result + /** + * @param ratesMap a map of rates **{base currency}**-{currency} to rate pairs + */ data class Result( val ratesMap: ExchangeRatesMap, val provider: ExchangeProvider diff --git a/core/exchange-provider/src/main/java/com/ivy/exchange/coinbase/CoinbaseExchangeProvider.kt b/core/exchange-provider/src/main/java/com/ivy/exchange/coinbase/CoinbaseExchangeProvider.kt deleted file mode 100644 index 07ac67217b..0000000000 --- a/core/exchange-provider/src/main/java/com/ivy/exchange/coinbase/CoinbaseExchangeProvider.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.ivy.exchange.coinbase - -import com.ivy.data.CurrencyCode -import com.ivy.data.ExchangeRatesMap -import com.ivy.data.exchange.ExchangeProvider -import com.ivy.exchange.RemoteExchangeProvider -import com.ivy.network.ktorClient -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* -import javax.inject.Inject - -class CoinbaseExchangeProvider @Inject constructor() : RemoteExchangeProvider { - override suspend fun fetchExchangeRates( - baseCurrency: CurrencyCode - ): RemoteExchangeProvider.Result = RemoteExchangeProvider.Result( - ratesMap = fetchRates(baseCurrency), - provider = ExchangeProvider.Coinbase - ) - - private suspend fun fetchRates(baseCurrency: CurrencyCode): ExchangeRatesMap { - val response = ktorClient().get("https://api.coinbase.com/v2/exchange-rates") { - parameter("currency", baseCurrency) - } - - return if (response.status.isSuccess()) { - response.body().data.rates - } else { - // error - emptyMap() - } - } - - -} \ No newline at end of file diff --git a/core/exchange-provider/src/main/java/com/ivy/exchange/coinbase/CoinbaseRatesResponse.kt b/core/exchange-provider/src/main/java/com/ivy/exchange/coinbase/CoinbaseRatesResponse.kt deleted file mode 100644 index 9ab0535ce7..0000000000 --- a/core/exchange-provider/src/main/java/com/ivy/exchange/coinbase/CoinbaseRatesResponse.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.ivy.exchange.coinbase - -import com.google.gson.annotations.SerializedName - -data class CoinbaseRatesResponse( - @SerializedName("data") - val data: ExchangeRatesData -) - -data class ExchangeRatesData( - @SerializedName("currency") - val currency: String, - @SerializedName("rates") - val rates: Map -) \ No newline at end of file diff --git a/core/exchange-provider/src/main/java/com/ivy/exchange/di/ExchangeModuleDI.kt b/core/exchange-provider/src/main/java/com/ivy/exchange/di/ExchangeModuleDI.kt index 0324c3caaa..06864ae8a6 100644 --- a/core/exchange-provider/src/main/java/com/ivy/exchange/di/ExchangeModuleDI.kt +++ b/core/exchange-provider/src/main/java/com/ivy/exchange/di/ExchangeModuleDI.kt @@ -1,7 +1,7 @@ package com.ivy.exchange.di import com.ivy.exchange.RemoteExchangeProvider -import com.ivy.exchange.coinbase.CoinbaseExchangeProvider +import com.ivy.exchange.fawazahmed0.Fawazahmed0ExchangeProvider import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -11,5 +11,5 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) abstract class ExchangeModuleDI { @Binds - abstract fun exchangeProvider(coinbase: CoinbaseExchangeProvider): RemoteExchangeProvider + abstract fun exchangeProvider(provider: Fawazahmed0ExchangeProvider): RemoteExchangeProvider } \ No newline at end of file diff --git a/core/exchange-provider/src/main/java/com/ivy/exchange/fawazahmed0/Fawazahmed0ExchangeProvider.kt b/core/exchange-provider/src/main/java/com/ivy/exchange/fawazahmed0/Fawazahmed0ExchangeProvider.kt new file mode 100644 index 0000000000..c23cf83b3f --- /dev/null +++ b/core/exchange-provider/src/main/java/com/ivy/exchange/fawazahmed0/Fawazahmed0ExchangeProvider.kt @@ -0,0 +1,77 @@ +package com.ivy.exchange.fawazahmed0 + +import com.ivy.data.CurrencyCode +import com.ivy.data.exchange.ExchangeProvider +import com.ivy.exchange.RemoteExchangeProvider +import com.ivy.network.ktorClient +import io.ktor.client.call.* +import io.ktor.client.request.* +import javax.inject.Inject + +class Fawazahmed0ExchangeProvider @Inject constructor( + +) : RemoteExchangeProvider { + companion object { + private val FALLBACK_URLS = listOf( + "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/eur.json", + "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/eur.min.json", + "https://raw.githubusercontent.com/fawazahmed0/currency-api/1/latest/currencies/eur.min.json", + "https://raw.githubusercontent.com/fawazahmed0/currency-api/1/latest/currencies/eur.json", + ) + } + + override suspend fun fetchExchangeRates(baseCurrency: CurrencyCode): RemoteExchangeProvider.Result { + if (baseCurrency.isBlank()) return failure() + + var eurRates: Map = emptyMap() + for (url in FALLBACK_URLS) { + eurRates = fetchEurBaseRates(url) + if (eurRates.isNotEmpty()) break // rates fetched successfully, stop! + } + if (eurRates.isEmpty()) return failure() // empty rates = no rates = failure + + // At this point we must have non-empty EUR rates map + // Now we must convert them to base currency + /* + "eur": { + "bgn": 1.955902, + "usd": 1.062366, + } + */ + // the API works with lowercase currency codes + val baseCurrencyLower = baseCurrency.lowercase() + val eurBaseCurrRateNonZero = eurRates[baseCurrencyLower] + ?.takeIf { it > 0.0 } ?: return failure() + + val rates = eurRates.mapNotNull { (target, eurTargetRate) -> + try { + if (eurTargetRate > 0.0) { + val baseCurrencyTargetRate = eurTargetRate / eurBaseCurrRateNonZero + target.uppercase() to baseCurrencyTargetRate + } else null + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + return RemoteExchangeProvider.Result( + ratesMap = rates.toMap(), + provider = ExchangeProvider.Fawazahmed0 + ) + } + + private suspend fun fetchEurBaseRates(url: String): Map { + return try { + ktorClient().get(url).body().eur + } catch (e: Exception) { + e.printStackTrace() + emptyMap() + } + } + + private fun failure() = RemoteExchangeProvider.Result( + ratesMap = emptyMap(), + provider = ExchangeProvider.Fawazahmed0 + ) +} \ No newline at end of file diff --git a/core/exchange-provider/src/main/java/com/ivy/exchange/fawazahmed0/Fawazahmed0Response.kt b/core/exchange-provider/src/main/java/com/ivy/exchange/fawazahmed0/Fawazahmed0Response.kt new file mode 100644 index 0000000000..2036b4a6c5 --- /dev/null +++ b/core/exchange-provider/src/main/java/com/ivy/exchange/fawazahmed0/Fawazahmed0Response.kt @@ -0,0 +1,10 @@ +package com.ivy.exchange.fawazahmed0 + +import com.google.gson.annotations.SerializedName + +data class Fawazahmed0Response( + @SerializedName("date") + val date: String, + @SerializedName("eur") + val eur: Map, +) \ No newline at end of file diff --git a/core/persistence/build.gradle.kts b/core/persistence/build.gradle.kts index 45176dd9f6..5065088f82 100644 --- a/core/persistence/build.gradle.kts +++ b/core/persistence/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { Hilt() implementation(project(":common:main")) RoomDB(api = false) - DataStore(api = false) + DataStore(api = true) Testing() } \ No newline at end of file diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountDao.kt b/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountDao.kt index e089ad2c9d..24953d409e 100644 --- a/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountDao.kt +++ b/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountDao.kt @@ -17,8 +17,17 @@ interface AccountDao { //endregion // region Select + @Query("SELECT * FROM accounts WHERE sync != $DELETING ORDER BY orderNum ASC") + suspend fun findAllSnapshot(): List + @Query("SELECT * FROM accounts WHERE sync != $DELETING ORDER BY orderNum ASC") fun findAll(): Flow> + + @Query("SELECT * FROM accounts WHERE sync != $DELETING AND id = :accountId") + suspend fun findById(accountId: String): AccountEntity? + + @Query("SELECT MAX(orderNum) FROM accounts") + suspend fun findMaxOrderNum(): Double? // endregion // region Update diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountFolderDao.kt b/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountFolderDao.kt index 7415b10a79..3d5dd0169e 100644 --- a/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountFolderDao.kt +++ b/core/persistence/src/main/java/com/ivy/core/persistence/dao/account/AccountFolderDao.kt @@ -1,7 +1,34 @@ package com.ivy.core.persistence.dao.account import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.ivy.core.persistence.entity.account.AccountFolderEntity +import com.ivy.data.DELETING +import com.ivy.data.SyncState +import kotlinx.coroutines.flow.Flow @Dao interface AccountFolderDao { + // region Save + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(values: List) + //endregion + + // region Select + @Query("SELECT * FROM account_folders WHERE sync != $DELETING ORDER BY orderNum ASC") + fun findAll(): Flow> + + @Query("SELECT * FROM account_folders WHERE sync != $DELETING AND id = :folderId") + suspend fun findById(folderId: String): AccountFolderEntity? + + @Query("SELECT MAX(orderNum) FROM account_folders") + suspend fun findMaxOrderNum(): Double? + // endregion + + // region Update + @Query("UPDATE account_folders SET sync = :sync WHERE id = :folderId") + suspend fun updateSync(folderId: String, sync: SyncState) + // endregion } \ No newline at end of file diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/dao/category/CategoryDao.kt b/core/persistence/src/main/java/com/ivy/core/persistence/dao/category/CategoryDao.kt index e61c4458be..6b7bf12e93 100644 --- a/core/persistence/src/main/java/com/ivy/core/persistence/dao/category/CategoryDao.kt +++ b/core/persistence/src/main/java/com/ivy/core/persistence/dao/category/CategoryDao.kt @@ -17,8 +17,14 @@ interface CategoryDao { // endregion // region Select + @Query("SELECT * FROM categories WHERE sync != $DELETING AND id = :categoryId") + suspend fun findById(categoryId: String): CategoryEntity? + @Query("SELECT * FROM categories WHERE sync != $DELETING ORDER BY orderNum ASC") fun findAll(): Flow> + + @Query("SELECT MAX(orderNum) FROM categories WHERE parentCategoryId IS NULL") + suspend fun findMaxNoParentOrderNum(): Double? // endregion // region Update diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/dao/trn/TrnLinkRecordDao.kt b/core/persistence/src/main/java/com/ivy/core/persistence/dao/trn/TrnLinkRecordDao.kt index 15d72f7493..5aeda57727 100644 --- a/core/persistence/src/main/java/com/ivy/core/persistence/dao/trn/TrnLinkRecordDao.kt +++ b/core/persistence/src/main/java/com/ivy/core/persistence/dao/trn/TrnLinkRecordDao.kt @@ -19,6 +19,9 @@ interface TrnLinkRecordDao { // region Select @Query("SELECT * FROM trn_links WHERE sync != $DELETING") fun findAll(): Flow> + + @Query("SELECT * FROM trn_links WHERE batchId = :batchId AND sync != $DELETING") + suspend fun findByBatchId(batchId: String): List // endregion // region Update diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/datastore/keys/SettingsKeys.kt b/core/persistence/src/main/java/com/ivy/core/persistence/datastore/keys/SettingsKeys.kt index afdec9ebd9..2f25b3a4f6 100644 --- a/core/persistence/src/main/java/com/ivy/core/persistence/datastore/keys/SettingsKeys.kt +++ b/core/persistence/src/main/java/com/ivy/core/persistence/datastore/keys/SettingsKeys.kt @@ -11,5 +11,7 @@ class SettingsKeys @Inject constructor() { val baseCurrency by lazy { stringPreferencesKey(name = "base_currency") } val startDayOfMonth by lazy { intPreferencesKey(name = "start_day_of_month") } val hideBalance by lazy { booleanPreferencesKey(name = "hide_balance") } + val appLocked by lazy { booleanPreferencesKey(name = "app_locked") } val displayName by lazy { stringPreferencesKey(name = "display_name") } + val theme by lazy { intPreferencesKey(name = "theme") } } \ No newline at end of file diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnQueryExecutor.kt b/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnQueryExecutor.kt index c1e22bc8b4..58e3b80492 100644 --- a/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnQueryExecutor.kt +++ b/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnQueryExecutor.kt @@ -1,7 +1,7 @@ package com.ivy.core.persistence.query import androidx.sqlite.db.SimpleSQLiteQuery -import com.ivy.common.time.TimeProvider +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.persistence.dao.trn.TrnDao import com.ivy.core.persistence.entity.trn.TrnEntity import javax.inject.Inject diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnWhere.kt b/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnWhere.kt index 45ce1a6f04..8a35c9c5a6 100644 --- a/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnWhere.kt +++ b/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnWhere.kt @@ -1,7 +1,7 @@ package com.ivy.core.persistence.query import arrow.core.NonEmptyList -import com.ivy.common.time.TimeProvider +import com.ivy.common.time.provider.TimeProvider import com.ivy.common.time.toEpochSeconds import com.ivy.common.time.toPair import com.ivy.core.persistence.entity.trn.data.TrnTimeType @@ -27,6 +27,7 @@ sealed interface TrnWhere { data class BySync(val sync: SyncState) : TrnWhere data class ByPurpose(val purpose: TrnPurpose?) : TrnWhere + data class ByPurposeIn(val purposes: NonEmptyList) : TrnWhere /** * Inclusive period [from, to] @@ -86,9 +87,14 @@ internal fun generateWhereClause( ) is BySync -> "sync = ?" to arg(where.sync.code) + is ByPurpose -> where.purpose?.let { "purpose = ?" to arg(where.purpose.code) } ?: ("purpose IS NULL" to noArg()) + is ByPurposeIn -> + "purpose IN (${placeholders(where.purposes.size)})" to arg( + where.purposes.map { it.code }.toList() + ) is ByAccountId -> "accountId = ?" to arg(where.accountId) is ByAccountIdIn -> diff --git a/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnWhereBuiltIn.kt b/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnWhereBuiltIn.kt deleted file mode 100644 index c5ef33682d..0000000000 --- a/core/persistence/src/main/java/com/ivy/core/persistence/query/TrnWhereBuiltIn.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.ivy.core.persistence.query - -import com.ivy.common.time.beginningOfIvyTime -import com.ivy.common.time.timeNow -import com.ivy.core.persistence.query.TrnWhere.ActualBetween -import com.ivy.core.persistence.query.TrnWhere.DueBetween -import com.ivy.data.time.TimeRange -import java.time.LocalDateTime - -/** - * Everything due **[beginIvy, now]** including this moment is overdue. - */ -fun overdue(): TrnWhere = DueBetween( - TimeRange(from = beginningOfIvyTime(), to = timeNow()) -) - -/** - * Everything due **(now, endIvy]** from 1 second after this moment is upcoming. - */ -fun upcoming(to: LocalDateTime): TrnWhere = DueBetween( - TimeRange(from = timeNow().plusSeconds(1), to = to) -) - -fun trnsForPeriod(range: TimeRange): TrnWhere = - DueBetween(range) or ActualBetween(range) \ No newline at end of file diff --git a/core/persistence/src/test/java/com/ivy/core/persistence/query/TrnWhereTest.kt b/core/persistence/src/test/java/com/ivy/core/persistence/query/TrnWhereTest.kt index 419f64764a..bdf49d1992 100644 --- a/core/persistence/src/test/java/com/ivy/core/persistence/query/TrnWhereTest.kt +++ b/core/persistence/src/test/java/com/ivy/core/persistence/query/TrnWhereTest.kt @@ -41,8 +41,10 @@ class TrnWhereTest : StringSpec({ val genByAccount = arbitrary { ByAccountId(genId.bind()) } val genTrnType = Arb.enum() + val genTrnPurpose = Arb.enum() val genByType = arbitrary { ByType(trnType = genTrnType.bind()) } + val genByPurpose = arbitrary { ByPurpose(purpose = genTrnPurpose.bind()) } val genBySync = arbitrary { BySync(sync = Arb.enum().bind()) } @@ -64,6 +66,10 @@ class TrnWhereTest : StringSpec({ ByTypeIn(Arb.nonEmptyList(genTrnType, 1..3).bind()) } + val genByPurposeIn = arbitrary { + ByPurposeIn(Arb.nonEmptyList(genTrnPurpose, 1..3).bind()) + } + val genSimpleQuery = Arb.choice( genById, genByType, @@ -273,6 +279,15 @@ class TrnWhereTest : StringSpec({ } } + "generate ByPurpose" { + checkAll(genByPurpose) { byPurpose -> + val res = generateWhereClause(byPurpose, timeProvider = timeProvider) + + res.query shouldBe "purpose = ?" + res.args shouldBe listOf(byPurpose.purpose?.code) + } + } + "generate BySync" { checkAll(genBySync) { bySync -> val res = generateWhereClause(bySync, timeProvider = timeProvider) @@ -384,6 +399,18 @@ class TrnWhereTest : StringSpec({ } } + "generate ByPurposeIn" { + checkAll(genByPurposeIn) { byPurposeIn -> + val purposes = byPurposeIn.purposes + + val res = generateWhereClause(byPurposeIn, timeProvider = timeProvider) + + res.query shouldBe "purpose IN (${placeholders(purposes.size)})" + res.args shouldBe purposes.toList().map { it.code } + } + } + + "generate Brackets" { checkAll(genBrackets) { brackets -> val condWhere = generateWhereClause(brackets, timeProvider = timeProvider) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index f5d759b28f..e6e9db2afd 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -14,8 +14,7 @@ dependencies { implementation(project(":design-system")) implementation(project(":core:domain")) implementation(project(":navigation")) - implementation(project(":app-base")) // TODO: temp dependency, remove later - implementation(project(":navigation")) + implementation(project(":math")) Testing() } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/LazyListUtil.kt b/core/ui/src/main/java/com/ivy/core/ui/LazyListUtil.kt new file mode 100644 index 0000000000..f2f053cd7c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/LazyListUtil.kt @@ -0,0 +1,23 @@ +package com.ivy.core.ui + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer + +fun LazyListScope.lastItemSpacerVertical( + height: Dp = 24.dp, +) { + item(key = "last_item_spacer") { + SpacerVer(height = height) + } +} + +fun LazyListScope.lastItemSpacerHorizontal( + width: Dp = 24.dp, +) { + item(key = "last_item_spacer") { + SpacerHor(width = width) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/ViewModelUtil.kt b/core/ui/src/main/java/com/ivy/core/ui/ViewModelUtil.kt new file mode 100644 index 0000000000..0e7c21ca17 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/ViewModelUtil.kt @@ -0,0 +1,11 @@ +package com.ivy.core.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import com.ivy.core.domain.FlowViewModel + +@Composable +inline fun uiStatePreviewSafe( + viewModel: FlowViewModel?, + preview: () -> UiState +): UiState = viewModel?.uiState?.collectAsState()?.value ?: preview() \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/AccountBadge.kt b/core/ui/src/main/java/com/ivy/core/ui/account/AccountBadge.kt index 2c498a8a0b..18e6ecb0a5 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/account/AccountBadge.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/account/AccountBadge.kt @@ -1,12 +1,13 @@ package com.ivy.core.ui.account import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import com.ivy.core.ui.R import com.ivy.core.ui.component.BadgeComponent -import com.ivy.core.ui.data.AccountUi -import com.ivy.core.ui.data.dummyAccountUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi import com.ivy.core.ui.data.icon.dummyIconSized import com.ivy.design.l0_system.color.Black import com.ivy.design.l0_system.color.Green @@ -15,10 +16,12 @@ import com.ivy.design.util.ComponentPreview @Composable fun AccountBadge( account: AccountUi, + modifier: Modifier = Modifier, background: Color = account.color, onClick: (() -> Unit)? = null ) { BadgeComponent( + modifier = modifier, icon = account.icon, text = account.name, background = background, diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/AccountButton.kt b/core/ui/src/main/java/com/ivy/core/ui/account/AccountButton.kt new file mode 100644 index 0000000000..ca2368b225 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/AccountButton.kt @@ -0,0 +1,79 @@ +package com.ivy.core.ui.account + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.util.ComponentPreview + +@Composable +fun AccountButton( + account: AccountUi, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Row( + modifier = modifier + .clip(UI.shapes.fullyRounded) + .background(account.color, UI.shapes.fullyRounded) + .clickable(onClick = onClick) + .padding(start = 8.dp, end = 16.dp) + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val contrast = rememberContrast(color = account.color) + ItemIcon( + itemIcon = account.icon, + size = IconSize.S, + tint = contrast, + ) + B2( + modifier = Modifier + .padding(start = 4.dp) + .widthIn(min = 0.dp, max = 120.dp), + text = account.name, + color = contrast, + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + AccountButton( + account = dummyAccountUi(), + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_Long() { + ComponentPreview { + AccountButton( + account = dummyAccountUi( + name = "This is a very long account name, which should be on multiple lines", + ), + onClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/AccountCard.kt b/core/ui/src/main/java/com/ivy/core/ui/account/AccountCard.kt deleted file mode 100644 index 94099e9ff6..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/account/AccountCard.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.ivy.core.ui.account - diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/BaseAccountModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/BaseAccountModal.kt new file mode 100644 index 0000000000..1bfd017fdf --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/BaseAccountModal.kt @@ -0,0 +1,239 @@ +package com.ivy.core.ui.account + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.account.create.components.AccountCurrency +import com.ivy.core.ui.account.create.components.AccountFolderButton +import com.ivy.core.ui.account.create.components.ExcludeAccount +import com.ivy.core.ui.account.create.components.ExcludedAccInfoModal +import com.ivy.core.ui.account.folder.pick.FolderPickerModal +import com.ivy.core.ui.color.ColorButton +import com.ivy.core.ui.color.picker.ColorPickerModal +import com.ivy.core.ui.component.ItemIconNameRow +import com.ivy.core.ui.currency.CurrencyPickerModal +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.data.icon.dummyIconSized +import com.ivy.core.ui.icon.picker.IconPickerModal +import com.ivy.data.CurrencyCode +import com.ivy.data.ItemIconId +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.DividerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l2_components.modal.scope.ModalActionsScope +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.util.IvyPreview + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun BoxScope.BaseAccountModal( + modal: IvyModal, + level: Int, + autoFocusNameInput: Boolean, + title: String, + nameInputHint: String, + positiveActionText: String, + secondaryActions: (@Composable ModalActionsScope.() -> Unit)? = null, + icon: ItemIcon, + initialName: String, + color: Color, + currency: CurrencyCode, + folder: FolderUi?, + excluded: Boolean, + contentBelow: (LazyListScope.() -> Unit)? = null, + onIconChange: (ItemIconId) -> Unit, + onNameChange: (String) -> Unit, + onColorChange: (Color) -> Unit, + onCurrencyChange: (CurrencyCode) -> Unit, + onFolderChange: (FolderUi?) -> Unit, + onExcludedChange: (Boolean) -> Unit, + onSaveAccount: (SaveAccountInfo) -> Unit, +) { + val iconPickerModal = rememberIvyModal() + val colorPickerModal = rememberIvyModal() + val currencyPickerModal = rememberIvyModal() + val excludedAccInfoModal = rememberIvyModal() + val chooseFolderModal = rememberIvyModal() + + val keyboardController = LocalSoftwareKeyboardController.current + Modal( + modal = modal, + level = level, + actions = { + secondaryActions?.invoke(this) + Positive( + text = positiveActionText, + feeling = Feeling.Custom(color) + ) { + onSaveAccount( + SaveAccountInfo( + color = color, + excluded = excluded, + folder = folder, + ) + ) + keyboardController?.hide() + modal.hide() + } + } + ) { + LazyColumn(modifier = Modifier.weight(1f)) { + item(key = "modal_title") { + Title(text = title) + SpacerVer(height = 24.dp) + } + item(key = "icon_name_color") { + // Keep in one item because so the title + // won't disappear on scroll + ItemIconNameRow( + icon = icon, + color = color, + initialName = initialName, + nameInputHint = nameInputHint, + autoFocusInput = autoFocusNameInput, + onPickIcon = { + keyboardController?.hide() + iconPickerModal.show() + }, + onNameChange = onNameChange, + ) + SpacerVer(height = 16.dp) + ColorButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = color + ) { + keyboardController?.hide() + colorPickerModal.show() + } + SpacerVer(height = 16.dp) + } + item(key = "acc_currency") { + AccountCurrency( + currency = currency, + color = color, + onPickCurrency = { + keyboardController?.hide() + currencyPickerModal.show() + } + ) + SpacerVer(height = 12.dp) + } + item(key = "acc_folder") { + AccountFolderButton( + folder = folder, + color = color, + ) { + keyboardController?.hide() + chooseFolderModal.show() + } + } + item(key = "line_divider") { + SpacerVer(height = 24.dp) + DividerHor() + SpacerVer(height = 12.dp) + } + item(key = "exclude_acc") { + ExcludeAccount( + excluded = excluded, + onMoreInfo = { + keyboardController?.hide() + excludedAccInfoModal.show() + }, + onExcludedChange = onExcludedChange, + ) + } + contentBelow?.invoke(this) + item(key = "last_item_spacer") { + SpacerVer(height = 48.dp) // last spacer + } + } + } + + IconPickerModal( + modal = iconPickerModal, + level = level + 1, + initialIcon = icon, + color = color, + onIconPick = onIconChange, + ) + ColorPickerModal( + modal = colorPickerModal, + level = level + 1, + initialColor = color, + onColorPicked = onColorChange, + ) + CurrencyPickerModal( + modal = currencyPickerModal, + level = level + 1, + initialCurrency = currency, + onCurrencyPick = onCurrencyChange, + ) + ExcludedAccInfoModal( + modal = excludedAccInfoModal, + level = level + 1, + ) + FolderPickerModal( + modal = chooseFolderModal, + level = level + 1, + selected = folder, + onPickFolder = onFolderChange, + ) +} + +data class SaveAccountInfo( + val color: Color, + val excluded: Boolean, + val folder: FolderUi? +) + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + BaseAccountModal( + modal = modal, + level = 1, + autoFocusNameInput = false, + title = stringResource(R.string.edit_account), + nameInputHint = stringResource(R.string.account_name), + positiveActionText = stringResource(R.string.save), + icon = dummyIconSized(R.drawable.ic_custom_account_m), + color = UI.colors.primary, + initialName = "Account", + excluded = false, + folder = null, + currency = "USD", + onNameChange = {}, + onIconChange = {}, + onCurrencyChange = {}, + onSaveAccount = {}, + onColorChange = {}, + onExcludedChange = {}, + onFolderChange = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceEvent.kt new file mode 100644 index 0000000000..f5e2ed094e --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceEvent.kt @@ -0,0 +1,12 @@ +package com.ivy.core.ui.account.adjustbalance + +import com.ivy.core.ui.account.adjustbalance.data.AdjustType +import com.ivy.data.Value + +internal sealed interface AdjustBalanceEvent { + data class Initial(val accountId: String) : AdjustBalanceEvent + + data class AdjustTypeChange(val type: AdjustType) : AdjustBalanceEvent + + data class AdjustBalance(val balance: Value) : AdjustBalanceEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceModal.kt new file mode 100644 index 0000000000..79b731b22f --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceModal.kt @@ -0,0 +1,179 @@ +package com.ivy.core.ui.account.adjustbalance + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.dummy.dummyValue +import com.ivy.core.ui.account.adjustbalance.data.AdjustType +import com.ivy.core.ui.amount.AmountModal +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.data.Value +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.Caption +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l2_components.modal.scope.ModalScope +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.design.util.thenWhen + +@Composable +fun BoxScope.AdjustBalanceModal( + modal: IvyModal, + level: Int = 1, + balance: Value, + accountId: String, +) { + val viewModel: AdjustBalanceViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel, preview = ::previewState) + + LaunchedEffect(accountId) { + viewModel?.onEvent( + AdjustBalanceEvent.Initial( + accountId = accountId, + ) + ) + } + + val calculatorVisible = remember { mutableStateOf(false) } + + AmountModal( + modal = modal, + level = level, + calculatorVisible = calculatorVisible, + contentAbove = { + Header( + type = state.adjustType, + onAdjustTypeChange = { + viewModel?.onEvent(AdjustBalanceEvent.AdjustTypeChange(it)) + } + ) + }, + initialAmount = balance, + onAmountEnter = { + viewModel?.onEvent( + AdjustBalanceEvent.AdjustBalance( + balance = it, + ) + ) + } + ) +} + +@Composable +private fun ModalScope.Header( + type: AdjustType, + onAdjustTypeChange: (AdjustType) -> Unit, +) { + Title(text = "Adjust balance") + SpacerVer(height = 8.dp) + AdjustType( + type = type, + onAdjustTypeChange = onAdjustTypeChange, + ) + SpacerVer(height = 12.dp) +} + +@Composable +private fun AdjustType( + type: AdjustType, + onAdjustTypeChange: (AdjustType) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AdjustTypeButton( + modifier = Modifier.weight(1f), + title = "Transaction", + desc = "Adjust transaction will be created.", + selected = type == AdjustType.WithTransaction + ) { + onAdjustTypeChange(AdjustType.WithTransaction) + } + SpacerHor(width = 8.dp) + AdjustTypeButton( + modifier = Modifier.weight(1f), + title = "Artificially", + desc = "No transaction will be created.", + selected = type == AdjustType.NoTransaction + ) { + onAdjustTypeChange(AdjustType.NoTransaction) + } + } +} + +@Composable +private fun AdjustTypeButton( + title: String, + desc: String, + selected: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Column( + modifier = modifier + .clip(UI.shapes.squared) + .thenWhen { + when (selected) { + true -> background(UI.colors.primary, UI.shapes.squared) + false -> border(1.dp, UI.colors.primary, UI.shapes.squared) + } + } + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + val textColor = if (selected) + rememberContrast(UI.colors.primary) else UI.colorsInverted.pure + B2( + modifier = Modifier.fillMaxWidth(), + text = title, + color = textColor, + maxLines = 1, + ) + SpacerVer(height = 4.dp) + Caption( + modifier = Modifier.fillMaxWidth(), + text = desc, + color = if (selected) textColor else UI.colors.neutral, + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + AdjustBalanceModal( + modal = modal, + balance = dummyValue(0.0), + accountId = "", + ) + } +} + +private fun previewState() = AdjustBalanceState( + adjustType = AdjustType.WithTransaction, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceState.kt new file mode 100644 index 0000000000..2d44e5105a --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.account.adjustbalance + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.account.adjustbalance.data.AdjustType + +@Immutable +internal data class AdjustBalanceState( + val adjustType: AdjustType, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceViewModel.kt new file mode 100644 index 0000000000..4d5337ad89 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/AdjustBalanceViewModel.kt @@ -0,0 +1,102 @@ +package com.ivy.core.ui.account.adjustbalance + +import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.action.account.AccountsFlow +import com.ivy.core.domain.action.account.AdjustAccBalanceAct +import com.ivy.core.domain.action.exchange.ExchangeRatesFlow +import com.ivy.core.domain.pure.exchange.exchange +import com.ivy.core.ui.account.adjustbalance.AdjustBalanceViewModel.State +import com.ivy.core.ui.account.adjustbalance.data.AdjustType +import com.ivy.data.account.Account +import com.ivy.data.exchange.ExchangeRatesData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +internal class AdjustBalanceViewModel @Inject constructor( + exchangeRatesFlow: ExchangeRatesFlow, + private val adjustAccBalanceAct: AdjustAccBalanceAct, + private val accountsFlow: AccountsFlow, +) : FlowViewModel() { + override val initialState: State = State( + ratesData = ExchangeRatesData( + baseCurrency = "", + rates = emptyMap(), + ), + account = null, + ) + + override val initialUi = AdjustBalanceState( + adjustType = AdjustType.WithTransaction, + ) + + private val accountId = MutableStateFlow("") + private val adjustType = MutableStateFlow(AdjustType.WithTransaction) + + override val stateFlow: Flow = combine( + exchangeRatesFlow(), accountFlow(), + ) { ratesData, account -> + State( + ratesData = ratesData, + account = account, + ) + } + + private fun accountFlow(): Flow = combine( + accountsFlow(), accountId + ) { accounts, accountId -> + accounts.firstOrNull { it.id.toString() == accountId } + } + + override val uiFlow: Flow = adjustType.map { adjustType -> + AdjustBalanceState( + adjustType = adjustType, + ) + } + + // region Event handling + override suspend fun handleEvent(event: AdjustBalanceEvent) = when (event) { + is AdjustBalanceEvent.Initial -> handleInitial(event) + is AdjustBalanceEvent.AdjustBalance -> handleAdjustBalance(event) + is AdjustBalanceEvent.AdjustTypeChange -> handleAdjustTypeChange(event) + } + + private fun handleInitial(event: AdjustBalanceEvent.Initial) { + accountId.value = event.accountId + } + + private suspend fun handleAdjustBalance(event: AdjustBalanceEvent.AdjustBalance) { + val account = state.value.account ?: return + val accountAmount = exchange( + exchangeData = state.value.ratesData, + from = event.balance.currency, + to = account.currency, + amount = event.balance.amount + ).orNull() ?: return + + adjustAccBalanceAct( + AdjustAccBalanceAct.Input( + account = account, + desiredBalance = accountAmount, + hideTransaction = when (adjustType.value) { + AdjustType.WithTransaction -> false + AdjustType.NoTransaction -> true + } + ) + ) + } + + private fun handleAdjustTypeChange(event: AdjustBalanceEvent.AdjustTypeChange) { + adjustType.value = event.type + } + // endregion + + data class State( + val ratesData: ExchangeRatesData, + val account: Account?, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/data/AdjustType.kt b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/data/AdjustType.kt new file mode 100644 index 0000000000..0ba0f7254a --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/adjustbalance/data/AdjustType.kt @@ -0,0 +1,8 @@ +package com.ivy.core.ui.account.adjustbalance.data + +import androidx.compose.runtime.Immutable + +@Immutable +enum class AdjustType { + WithTransaction, NoTransaction +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountEvent.kt new file mode 100644 index 0000000000..d85b9e718f --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountEvent.kt @@ -0,0 +1,20 @@ +package com.ivy.core.ui.account.create + +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.data.CurrencyCode +import com.ivy.data.ItemIconId + +internal sealed interface CreateAccountEvent { + data class CreateAccount( + val color: Color, + val excluded: Boolean, + val folder: FolderUi? + ) : CreateAccountEvent + + data class IconChange(val iconId: ItemIconId) : CreateAccountEvent + + data class NameChange(val name: String) : CreateAccountEvent + + data class CurrencyChange(val newCurrency: CurrencyCode) : CreateAccountEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountModal.kt new file mode 100644 index 0000000000..4339172560 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountModal.kt @@ -0,0 +1,78 @@ +package com.ivy.core.ui.account.create + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.* +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.core.ui.R +import com.ivy.core.ui.account.BaseAccountModal +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.icon.dummyIconSized +import com.ivy.design.l0_system.UI +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@Composable +fun BoxScope.CreateAccountModal( + modal: IvyModal, + level: Int = 1 +) { + val viewModel: CreateAccountViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() + + val primary = UI.colors.primary + var color by remember(primary) { mutableStateOf(primary) } + var excluded by remember { mutableStateOf(false) } + var folder by remember { mutableStateOf(null) } + + val newAccountString = stringResource(R.string.new_account) + BaseAccountModal( + modal = modal, + level = level, + autoFocusNameInput = true, + title = newAccountString, + nameInputHint = newAccountString, + positiveActionText = stringResource(R.string.add_account), + icon = state.icon, + initialName = "", + currency = state.currency, + color = color, + excluded = excluded, + folder = folder, + onNameChange = { viewModel?.onEvent(CreateAccountEvent.NameChange(it)) }, + onIconChange = { viewModel?.onEvent(CreateAccountEvent.IconChange(it)) }, + onCurrencyChange = { viewModel?.onEvent(CreateAccountEvent.CurrencyChange(it)) }, + onFolderChange = { folder = it }, + onExcludedChange = { excluded = it }, + onColorChange = { color = it }, + onSaveAccount = { + viewModel?.onEvent( + CreateAccountEvent.CreateAccount( + color = it.color, + excluded = it.excluded, + folder = it.folder + ) + ) + } + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + CreateAccountModal(modal = modal) + } +} + +private fun previewState() = CreateAccountState( + currency = "USD", + icon = dummyIconSized(R.drawable.ic_custom_account_m) +) +// endregion diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountState.kt new file mode 100644 index 0000000000..2a4f8b10ca --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountState.kt @@ -0,0 +1,11 @@ +package com.ivy.core.ui.account.create + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.CurrencyCode + +@Immutable +internal data class CreateAccountState( + val currency: CurrencyCode, + val icon: ItemIcon +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountViewModel.kt new file mode 100644 index 0000000000..0f96e9b6d0 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/create/CreateAccountViewModel.kt @@ -0,0 +1,92 @@ +package com.ivy.core.ui.account.create + +import androidx.compose.ui.graphics.toArgb +import com.ivy.common.toUUID +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.NewAccountTabItemOrderNumAct +import com.ivy.core.domain.action.account.WriteAccountsAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow +import com.ivy.core.ui.R +import com.ivy.core.ui.action.DefaultTo +import com.ivy.core.ui.action.ItemIconAct +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.CurrencyCode +import com.ivy.data.ItemIconId +import com.ivy.data.SyncState +import com.ivy.data.account.Account +import com.ivy.data.account.AccountState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import java.util.* +import javax.inject.Inject + +@HiltViewModel +internal class CreateAccountViewModel @Inject constructor( + private val itemIconAct: ItemIconAct, + private val writeAccountsAct: WriteAccountsAct, + private val newAccountTabItemOrderNumAct: NewAccountTabItemOrderNumAct, + baseCurrencyFlow: BaseCurrencyFlow, +) : SimpleFlowViewModel() { + override val initialUi = CreateAccountState( + currency = "", + icon = ItemIcon.Sized( + iconS = R.drawable.ic_custom_account_s, + iconM = R.drawable.ic_custom_account_m, + iconL = R.drawable.ic_custom_account_l, + iconId = null + ) + ) + + private var name = "" + private val currency = MutableStateFlow(null) + private val iconId = MutableStateFlow(null) + + override val uiFlow: Flow = combine( + baseCurrencyFlow(), currency, iconId + ) { baseCurrency, currency, iconId -> + CreateAccountState( + currency = currency ?: baseCurrency, + icon = itemIconAct(ItemIconAct.Input(iconId, DefaultTo.Account)) + ) + } + + // region Event Handling + override suspend fun handleEvent(event: CreateAccountEvent) = when (event) { + is CreateAccountEvent.CreateAccount -> createAccount(event) + is CreateAccountEvent.IconChange -> handleIconPick(event) + is CreateAccountEvent.NameChange -> handleNameChange(event) + is CreateAccountEvent.CurrencyChange -> handleCurrencyChange(event) + } + + private suspend fun createAccount(event: CreateAccountEvent.CreateAccount) { + val newAccount = Account( + id = UUID.randomUUID(), + name = name, + currency = uiState.value.currency, + color = event.color.toArgb(), + icon = iconId.value, + excluded = event.excluded, + folderId = event.folder?.id?.toUUID(), + orderNum = newAccountTabItemOrderNumAct(Unit), + state = AccountState.Default, + sync = SyncState.Syncing + ) + writeAccountsAct(Modify.save(newAccount)) + } + + private fun handleIconPick(event: CreateAccountEvent.IconChange) { + iconId.value = event.iconId + } + + private fun handleNameChange(event: CreateAccountEvent.NameChange) { + name = event.name + } + + private fun handleCurrencyChange(event: CreateAccountEvent.CurrencyChange) { + currency.value = event.newCurrency + } + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/create/components/AccountCurrency.kt b/core/ui/src/main/java/com/ivy/core/ui/account/create/components/AccountCurrency.kt new file mode 100644 index 0000000000..2d1da23b0a --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/create/components/AccountCurrency.kt @@ -0,0 +1,60 @@ +package com.ivy.core.ui.account.create.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.data.CurrencyCode +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +@Composable +internal fun ColumnScope.AccountCurrency( + currency: CurrencyCode, + color: Color, + modifier: Modifier = Modifier, + onPickCurrency: () -> Unit +) { + B1( + modifier = Modifier.padding(start = 24.dp), + text = "Account currency" + ) + SpacerVer(height = 8.dp) + IvyButton( + modifier = modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Custom(color), + text = currency, + icon = R.drawable.round_currency_exchange_24, + onClick = onPickCurrency + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + Column { + AccountCurrency( + currency = "BGN", + color = Purple, + onPickCurrency = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/create/components/AccountFolder.kt b/core/ui/src/main/java/com/ivy/core/ui/account/create/components/AccountFolder.kt new file mode 100644 index 0000000000..cda59350f1 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/create/components/AccountFolder.kt @@ -0,0 +1,80 @@ +package com.ivy.core.ui.account.create.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.account.folder.pick.FolderItem +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.account.dummyFolderUi +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +@Composable +internal fun ColumnScope.AccountFolderButton( + folder: FolderUi?, + modifier: Modifier = Modifier, + color: Color, + onClick: () -> Unit +) { + B1( + modifier = Modifier.padding(start = 24.dp), + text = "Folder" + ) + SpacerVer(height = 8.dp) + if (folder != null) { + FolderItem( + folder = folder, + selected = true, + onClick = onClick, + ) + } else { + IvyButton( + modifier = modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Custom(color), + text = "Choose folder", + icon = R.drawable.ic_vue_files_folder, + onClick = onClick + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview_None() { + ComponentPreview { + Column { + AccountFolderButton(folder = null, color = Purple) {} + } + } +} + +@Preview +@Composable +private fun Preview_Selected() { + ComponentPreview { + Column { + AccountFolderButton( + folder = dummyFolderUi("Business"), + color = Purple, + onClick = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/create/components/ExcludeAccount.kt b/core/ui/src/main/java/com/ivy/core/ui/account/create/components/ExcludeAccount.kt new file mode 100644 index 0000000000..84048e1a3c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/create/components/ExcludeAccount.kt @@ -0,0 +1,105 @@ +package com.ivy.core.ui.account.create.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.Switch +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Body +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.MoreInfoButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.IvyPreview + +@Composable +internal fun ExcludeAccount( + excluded: Boolean, + modifier: Modifier = Modifier, + onMoreInfo: () -> Unit, + onExcludedChange: (excluded: Boolean) -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(UI.shapes.fullyRounded) + .clickable { onExcludedChange(!excluded) }, + verticalAlignment = Alignment.CenterVertically + ) { + Switch( + enabled = excluded, + enabledColor = UI.colors.red, + onEnabledChange = onExcludedChange + ) + B2( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp, end = 4.dp), + text = "Exclude account", + fontWeight = FontWeight.ExtraBold, + color = UI.colorsInverted.pure, + ) + MoreInfoButton(onClick = onMoreInfo) + } +} + +@Composable +internal fun BoxScope.ExcludedAccInfoModal( + modal: IvyModal, + level: Int = 1, +) { + Modal( + modal = modal, + level = level, + actions = { + Positive(text = "Got it") { + modal.hide() + } + } + ) { + Title(text = "Excluded accounts") + SpacerVer(height = 24.dp) + Body( + text = "Excluded accounts don't count to your balance" + + " that you see on the \"Home\" screen. However, they're calculated" + + " in your expenses and you can still add transactions in them." + ) + SpacerVer(height = 48.dp) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + ExcludeAccount(excluded = false, onMoreInfo = { }, onExcludedChange = {}) + } +} + +@Preview +@Composable +private fun Preview_InfoModal() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + ExcludedAccInfoModal(modal = modal) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountEvent.kt new file mode 100644 index 0000000000..cc950c4c4a --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountEvent.kt @@ -0,0 +1,28 @@ +package com.ivy.core.ui.account.edit + +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.data.CurrencyCode +import com.ivy.data.ItemIconId + +internal sealed interface EditAccountEvent { + data class Initial(val accountId: String) : EditAccountEvent + + object EditAccount : EditAccountEvent + + data class IconChange(val iconId: ItemIconId) : EditAccountEvent + + data class NameChange(val name: String) : EditAccountEvent + + data class CurrencyChange(val newCurrency: CurrencyCode) : EditAccountEvent + + data class ColorChange(val color: Color) : EditAccountEvent + + data class FolderChange(val folder: FolderUi?) : EditAccountEvent + + data class ExcludedChange(val excluded: Boolean) : EditAccountEvent + + object Archive : EditAccountEvent + object Unarchive : EditAccountEvent + object Delete : EditAccountEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountModal.kt new file mode 100644 index 0000000000..0d605739be --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountModal.kt @@ -0,0 +1,203 @@ +package com.ivy.core.ui.account.edit + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.dummy.dummyValue +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.R +import com.ivy.core.ui.account.BaseAccountModal +import com.ivy.core.ui.account.adjustbalance.AdjustBalanceModal +import com.ivy.core.ui.account.edit.components.DeleteAccountModal +import com.ivy.core.ui.data.icon.dummyIconSized +import com.ivy.core.ui.value.AmountCurrency +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ArchiveButton +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.DeleteButton +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun BoxScope.EditAccountModal( + modal: IvyModal, + accountId: String, + level: Int = 1, +) { + val viewModel: EditAccountViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() + + LaunchedEffect(accountId) { + viewModel?.onEvent(EditAccountEvent.Initial(accountId)) + } + + val deleteAccountModal = rememberIvyModal() + val adjustBalanceModal = rememberIvyModal() + + val keyboardController = LocalSoftwareKeyboardController.current + BaseAccountModal( + modal = modal, + level = level, + autoFocusNameInput = false, + title = stringResource(R.string.edit_account), + nameInputHint = stringResource(R.string.account_name), + positiveActionText = stringResource(R.string.save), + secondaryActions = { + ArchiveButton( + archived = state.archived, + color = state.color, + onArchive = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditAccountEvent.Archive) + }, + onUnarchive = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditAccountEvent.Unarchive) + } + ) + SpacerHor(width = 8.dp) + DeleteButton { + keyboardController?.hide() + deleteAccountModal.show() + } + SpacerHor(width = 12.dp) + }, + icon = state.icon, + initialName = state.initialName, + currency = state.currency, + color = state.color, + excluded = state.excluded, + folder = state.folder, + contentBelow = { + item(key = "adjust_balance") { + AdjustBalance( + balance = state.balanceUi, + color = state.color + ) { + adjustBalanceModal.show() + } + } + }, + onNameChange = { viewModel?.onEvent(EditAccountEvent.NameChange(it)) }, + onIconChange = { viewModel?.onEvent(EditAccountEvent.IconChange(it)) }, + onCurrencyChange = { viewModel?.onEvent(EditAccountEvent.CurrencyChange(it)) }, + onFolderChange = { viewModel?.onEvent(EditAccountEvent.FolderChange(it)) }, + onExcludedChange = { viewModel?.onEvent(EditAccountEvent.ExcludedChange(it)) }, + onColorChange = { viewModel?.onEvent(EditAccountEvent.ColorChange(it)) }, + onSaveAccount = { viewModel?.onEvent(EditAccountEvent.EditAccount) } + ) + + DeleteAccountModal( + modal = deleteAccountModal, + level = level + 1, + accountName = state.initialName, + archived = state.archived, + onArchive = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditAccountEvent.Archive) + }, + onDelete = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditAccountEvent.Delete) + } + ) + AdjustBalanceModal( + modal = adjustBalanceModal, + level = level + 1, + balance = state.balance, + accountId = state.accountId, + ) +} + +// region Adjust balance +@Composable +private fun AdjustBalance( + balance: ValueUi, + color: Color, + onClick: () -> Unit +) { + SpacerVer(height = 24.dp) + B1( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + text = "Account's balance", + textAlign = TextAlign.Center, + color = color, + ) + SpacerVer(height = 8.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + AmountCurrency(balance) + } + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Custom(color), + text = stringResource(R.string.adjust_balance), + icon = R.drawable.ic_vue_money_coins, + onClick = onClick + ) +} +// endregion + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + EditAccountModal( + modal = modal, + accountId = "" + ) + } +} + +private fun previewState() = EditAccountState( + accountId = "", + currency = "USD", + icon = dummyIconSized(R.drawable.ic_custom_account_m), + initialName = "Account", + folder = null, + excluded = false, + color = Purple, + archived = false, + balance = dummyValue(1_000.0), + balanceUi = dummyValueUi("1,000.00") +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountState.kt new file mode 100644 index 0000000000..7e7b2dd908 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountState.kt @@ -0,0 +1,23 @@ +package com.ivy.core.ui.account.edit + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.CurrencyCode +import com.ivy.data.Value + +@Immutable +internal data class EditAccountState( + val accountId: String, + val currency: CurrencyCode, + val icon: ItemIcon, + val color: Color, + val initialName: String, + val folder: FolderUi?, + val excluded: Boolean, + val archived: Boolean, + val balance: Value, + val balanceUi: ValueUi, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountViewModel.kt new file mode 100644 index 0000000000..da4cd0cc99 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/edit/EditAccountViewModel.kt @@ -0,0 +1,231 @@ +package com.ivy.core.ui.account.edit + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.Toast +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.ivy.common.toUUID +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountByIdAct +import com.ivy.core.domain.action.account.WriteAccountsAct +import com.ivy.core.domain.action.account.folder.AccountFoldersFlow +import com.ivy.core.domain.action.calculate.account.AccBalanceFlow +import com.ivy.core.domain.action.data.AccountListItem +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.format +import com.ivy.core.ui.R +import com.ivy.core.ui.action.DefaultTo +import com.ivy.core.ui.action.ItemIconAct +import com.ivy.core.ui.action.mapping.account.MapFolderUiAct +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.CurrencyCode +import com.ivy.data.ItemIconId +import com.ivy.data.Value +import com.ivy.data.account.Account +import com.ivy.data.account.AccountState +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l0_system.color.toComposeColor +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@SuppressLint("StaticFieldLeak") +@HiltViewModel +internal class EditAccountViewModel @Inject constructor( + @ApplicationContext + private val appContext: Context, + private val itemIconAct: ItemIconAct, + private val writeAccountsAct: WriteAccountsAct, + private val accountByIdAct: AccountByIdAct, + private val accountFoldersFlow: AccountFoldersFlow, + private val mapFolderUiAct: MapFolderUiAct, + private val accBalanceFlow: AccBalanceFlow, +) : SimpleFlowViewModel() { + override val initialUi = EditAccountState( + accountId = "", + currency = "", + icon = ItemIcon.Sized( + iconS = R.drawable.ic_custom_account_s, + iconM = R.drawable.ic_custom_account_m, + iconL = R.drawable.ic_custom_account_l, + iconId = null + ), + color = Purple, + initialName = "", + folder = null, + excluded = false, + archived = false, + balance = Value(0.0, ""), + balanceUi = ValueUi("0.00", ""), + ) + + private val account = MutableStateFlow(null) + private var name = "" + private val initialName = MutableStateFlow(initialUi.initialName) + private val currency = MutableStateFlow(initialUi.currency) + private val iconId = MutableStateFlow(null) + private val color = MutableStateFlow(initialUi.color) + private val excluded = MutableStateFlow(initialUi.excluded) + private val folderId = MutableStateFlow(null) + private val archived = MutableStateFlow(initialUi.archived) + + override val uiFlow: Flow = combine( + account, headerFlow(), secondaryFlow(), folderFlow(), accountBalanceFlow() + ) { account, header, secondary, folder, balance -> + EditAccountState( + accountId = account?.id?.toString() ?: "", + currency = secondary.currency, + icon = itemIconAct(ItemIconAct.Input(header.iconId, DefaultTo.Account)), + initialName = header.initialName, + color = header.color, + excluded = secondary.excluded, + folder = folder, + archived = secondary.archived, + balance = balance ?: initialUi.balance, + balanceUi = balance?.let { format(it, shortenFiat = false) } ?: initialUi.balanceUi + ) + } + + private fun headerFlow(): Flow
= combine( + iconId, initialName, color, + ) { iconId, initialName, color -> + Header(iconId = iconId, initialName = initialName, color = color) + } + + private fun secondaryFlow(): Flow = combine( + currency, excluded, archived + ) { currency, excluded, archived -> + Secondary(currency, excluded, archived) + } + + private fun folderFlow(): Flow = combine( + accountFoldersFlow(Unit), folderId + ) { folders, folderId -> + folders.filterIsInstance() + .firstOrNull { it.folder.id == folderId } + ?.let { mapFolderUiAct(it.folder) } + } + + @OptIn(FlowPreview::class) + private fun accountBalanceFlow(): Flow = account.flatMapLatest { account -> + if (account != null) { + accBalanceFlow(AccBalanceFlow.Input(account)) + } else flowOf(null) + } + + // region Event Handling + override suspend fun handleEvent(event: EditAccountEvent) = when (event) { + is EditAccountEvent.Initial -> handleInitial(event) + EditAccountEvent.EditAccount -> editAccount() + is EditAccountEvent.IconChange -> handleIconPick(event) + is EditAccountEvent.NameChange -> handleNameChange(event) + is EditAccountEvent.CurrencyChange -> handleCurrencyChange(event) + is EditAccountEvent.ColorChange -> handleColorChange(event) + is EditAccountEvent.ExcludedChange -> handleExcludedChange(event) + is EditAccountEvent.FolderChange -> handleFolderChange(event) + EditAccountEvent.Archive -> handleArchive() + EditAccountEvent.Unarchive -> handleUnarchive() + EditAccountEvent.Delete -> handleDelete() + } + + private suspend fun handleInitial(event: EditAccountEvent.Initial) { + // we need a snapshot of the account at this given point in time + // => flow isn't good for that use-case + accountByIdAct(event.accountId)?.let { + account.value = it + name = it.name + initialName.value = it.name + currency.value = it.currency + iconId.value = it.icon + color.value = it.color.toComposeColor() + excluded.value = it.excluded + folderId.value = it.folderId?.toString() + archived.value = it.state == AccountState.Archived + } + } + + private suspend fun editAccount() { + val updatedAccount = account.value?.copy( + name = name, + currency = currency.value, + color = color.value.toArgb(), + folderId = folderId.value?.toUUID(), + excluded = excluded.value, + icon = iconId.value + ) + if (updatedAccount != null) { + writeAccountsAct(Modify.save(updatedAccount)) + } + } + + private fun handleIconPick(event: EditAccountEvent.IconChange) { + iconId.value = event.iconId + } + + private fun handleNameChange(event: EditAccountEvent.NameChange) { + name = event.name + } + + private fun handleCurrencyChange(event: EditAccountEvent.CurrencyChange) { + currency.value = event.newCurrency + } + + private fun handleColorChange(event: EditAccountEvent.ColorChange) { + color.value = event.color + } + + private fun handleFolderChange(event: EditAccountEvent.FolderChange) { + folderId.value = event.folder?.id + } + + private fun handleExcludedChange(event: EditAccountEvent.ExcludedChange) { + excluded.value = event.excluded + } + + private suspend fun handleArchive() { + archived.value = true + updateArchived(state = AccountState.Archived) + showToast("Account archived") + } + + private suspend fun handleUnarchive() { + archived.value = false + updateArchived(state = AccountState.Default) + showToast("Account unarchived") + } + + private fun showToast(text: String) { + Toast.makeText(appContext, text, Toast.LENGTH_LONG).show() + } + + private suspend fun updateArchived(state: AccountState) { + val updatedAccount = account.value?.copy(state = state) + if (updatedAccount != null) { + writeAccountsAct(Modify.save(updatedAccount)) + } + } + + private suspend fun handleDelete() { + account.value?.let { + writeAccountsAct(Modify.delete(it.id.toString())) + } + } + // endregion + + private data class Header( + val iconId: ItemIconId?, + val initialName: String, + val color: Color, + ) + + private data class Secondary( + val currency: CurrencyCode, + val excluded: Boolean, + val archived: Boolean, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/edit/components/DeleteAccountModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/edit/components/DeleteAccountModal.kt new file mode 100644 index 0000000000..c380de4456 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/edit/components/DeleteAccountModal.kt @@ -0,0 +1,123 @@ +package com.ivy.core.ui.account.edit.components + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Body +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.DeleteAccountModal( + modal: IvyModal, + level: Int = 1, + archived: Boolean, + accountName: String, + onArchive: () -> Unit, + onDelete: () -> Unit, +) { + Modal( + modal = modal, + level = level, + actions = { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Focused, + feeling = Feeling.Negative, + text = "Delete forever", + icon = R.drawable.ic_round_delete_forever_24 + ) { + modal.hide() + onDelete() + } + } + ) { + Title( + text = "Delete \"$accountName\" account forever?", + color = UI.colors.red + ) + SpacerVer(height = 24.dp) + Body( + text = bodyText( + accountName = accountName, + archived = archived + ) + ) + if (!archived) { + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "Archive", + icon = R.drawable.round_archive_24 + ) { + modal.hide() + onArchive() + } + } + SpacerVer(height = 48.dp) + } +} + +private fun bodyText( + accountName: String, + archived: Boolean +): String { + val baseText = "DANGER! Deleting \"$accountName\" account will delete all transactions" + + " in it forever. This operation CANNOT be undone and will affect your balance!" + + " Please, be careful otherwise you may lose your data." + + val unarchivedText = + "\n\nIf you don't want to see this account but want preserve its transactions," + + " a better option would be to just archive it." + return if (archived) baseText else baseText + unarchivedText +} + +// region Preview +@Preview +@Composable +private fun Preview_Unarchived() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + DeleteAccountModal( + modal = modal, + accountName = "Account 1", + archived = false, + onArchive = {}, + onDelete = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Archived() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + DeleteAccountModal( + modal = modal, + accountName = "Account 1", + archived = true, + onArchive = {}, + onDelete = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/BaseFolderModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/BaseFolderModal.kt new file mode 100644 index 0000000000..06b0c29f38 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/BaseFolderModal.kt @@ -0,0 +1,198 @@ +package com.ivy.core.ui.account.folder + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.account.pick.AccountPickerColumn +import com.ivy.core.ui.color.ColorButton +import com.ivy.core.ui.color.picker.ColorPickerModal +import com.ivy.core.ui.component.ItemIconNameRow +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.data.icon.dummyIconUnknown +import com.ivy.core.ui.icon.picker.IconPickerModal +import com.ivy.data.ItemIconId +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.DividerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l2_components.modal.scope.ModalActionsScope +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.util.IvyPreview + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun BoxScope.BaseFolderModal( + modal: IvyModal, + level: Int, + autoFocusNameInput: Boolean, + title: String, + positiveButtonText: String, + secondaryActions: (@Composable ModalActionsScope.() -> Unit)? = null, + initialName: String, + icon: ItemIcon, + color: Color, + accounts: List, + onNameChane: (String) -> Unit, + onColorChange: (Color) -> Unit, + onIconChange: (ItemIconId) -> Unit, + onAccountsChange: (List) -> Unit, + onSave: (SaveFolderInfo) -> Unit, +) { + val iconPickerModal = rememberIvyModal() + val colorPickerModal = rememberIvyModal() + + val keyboardController = LocalSoftwareKeyboardController.current + Modal( + modal = modal, + level = level, + actions = { + secondaryActions?.invoke(this) + Positive( + text = positiveButtonText, + feeling = Feeling.Custom(color) + ) { + onSave(SaveFolderInfo(color)) + keyboardController?.hide() + modal.hide() + } + } + ) { + LazyColumn(modifier = Modifier.weight(1f)) { + item(key = "modal_title") { + Title(text = title) + SpacerVer(height = 24.dp) + } + item(key = "icon_name_color") { + // Keep in one item because so the title + // won't disappear on scroll + ItemIconNameRow( + icon = icon, + color = color, + initialName = initialName, + nameInputHint = "Folder name", + autoFocusInput = autoFocusNameInput, + onPickIcon = { + keyboardController?.hide() + iconPickerModal.show() + }, + onNameChange = onNameChane + ) + SpacerVer(height = 16.dp) + ColorButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = color + ) { + keyboardController?.hide() + colorPickerModal.show() + } + SpacerVer(height = 24.dp) + } + item(key = "accounts_in_folder") { + // Can't have create account modal + // because of infinite recursion + AccountsInFolder( + selected = accounts, + createAccountModal = null, + onSelectedChange = onAccountsChange + ) + } + } + } + + IconPickerModal( + modal = iconPickerModal, + level = level + 1, + initialIcon = icon, + color = color, + onIconPick = onIconChange, + ) + + ColorPickerModal( + modal = colorPickerModal, + level = level + 1, + initialColor = color, + onColorPicked = onColorChange, + ) +} + +data class SaveFolderInfo( + val color: Color, +) + +@Composable +private fun ColumnScope.AccountsInFolder( + selected: List, + createAccountModal: IvyModal?, + onSelectedChange: (List) -> Unit, +) { + DividerHor() + SpacerVer(height = 12.dp) + B1( + modifier = Modifier.padding(start = 24.dp), + text = "Accounts in folder", + fontWeight = FontWeight.ExtraBold + ) + SpacerVer(height = 12.dp) + AccountPickerColumn( + modifier = Modifier.padding(horizontal = 8.dp), + selected = selected, + deselectButton = true, + onAddAccount = null, + onSelectAccount = { + onSelectedChange(selected.plus(it)) + }, + onDeselectAccount = { deselected -> + onSelectedChange(selected.filter { it.id != deselected.id }) + } + ) + SpacerVer(height = 24.dp) + DividerHor() + SpacerVer(height = 48.dp) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + BaseFolderModal( + modal = modal, + level = 1, + autoFocusNameInput = false, + title = "New folder", + positiveButtonText = "Add folder", + initialName = "", + icon = dummyIconUnknown(R.drawable.ic_vue_files_folder), + color = Purple, + accounts = listOf(), + onNameChane = {}, + onColorChange = {}, + onIconChange = {}, + onAccountsChange = {}, + onSave = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderEvent.kt new file mode 100644 index 0000000000..5597bbb820 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderEvent.kt @@ -0,0 +1,16 @@ +package com.ivy.core.ui.account.folder.create + +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.data.ItemIconId + +internal sealed interface CreateAccFolderEvent { + data class CreateFolder( + val color: Color, + val accounts: List, + ) : CreateAccFolderEvent + + data class NameChange(val name: String) : CreateAccFolderEvent + + data class IconChange(val iconId: ItemIconId) : CreateAccFolderEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderModal.kt new file mode 100644 index 0000000000..44e6a0df28 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderModal.kt @@ -0,0 +1,71 @@ +package com.ivy.core.ui.account.folder.create + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.* +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.core.ui.R +import com.ivy.core.ui.account.folder.BaseFolderModal +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@Composable +fun BoxScope.CreateAccFolderModal( + modal: IvyModal, + level: Int = 1, +) { + val viewModel: CreateAccFolderViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() + + val primary = UI.colors.primary + var folderColor by remember(primary) { mutableStateOf(primary) } + var accounts by remember { mutableStateOf>(emptyList()) } + + BaseFolderModal( + modal = modal, + level = level, + autoFocusNameInput = true, + title = "New folder", + positiveButtonText = "Add folder", + initialName = "", + icon = state.icon, + color = folderColor, + accounts = accounts, + onNameChane = { viewModel?.onEvent(CreateAccFolderEvent.NameChange(it)) }, + onColorChange = { folderColor = it }, + onIconChange = { viewModel?.onEvent(CreateAccFolderEvent.IconChange(it)) }, + onAccountsChange = { accounts = it }, + onSave = { + viewModel?.onEvent( + CreateAccFolderEvent.CreateFolder( + color = folderColor, + accounts = accounts + ) + ) + } + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + CreateAccFolderModal(modal = modal) + } +} + +private fun previewState() = CreateAccFolderState( + icon = ItemIcon.Unknown( + icon = R.drawable.ic_vue_files_folder, + iconId = "ic_vue_files_folder", + ) +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderState.kt new file mode 100644 index 0000000000..2de5685ce0 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.account.folder.create + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.icon.ItemIcon + +@Immutable +internal data class CreateAccFolderState( + val icon: ItemIcon +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderViewModel.kt new file mode 100644 index 0000000000..0bb048a9b3 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/create/CreateAccFolderViewModel.kt @@ -0,0 +1,78 @@ +package com.ivy.core.ui.account.folder.create + +import androidx.compose.ui.graphics.toArgb +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.NewAccountTabItemOrderNumAct +import com.ivy.core.domain.action.account.folder.WriteAccountFolderAct +import com.ivy.core.domain.action.account.folder.WriteAccountFolderContentAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.ui.R +import com.ivy.core.ui.action.DefaultTo +import com.ivy.core.ui.action.ItemIconAct +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.ItemIconId +import com.ivy.data.account.Folder +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import java.util.* +import javax.inject.Inject + +@HiltViewModel +internal class CreateAccFolderViewModel @Inject constructor( + private val itemIconAct: ItemIconAct, + private val writeAccountFolderAct: WriteAccountFolderAct, + private val writeAccountFolderContentAct: WriteAccountFolderContentAct, + private val newAccountTabItemOrderNumAct: NewAccountTabItemOrderNumAct, +) : SimpleFlowViewModel() { + override val initialUi = CreateAccFolderState( + icon = ItemIcon.Unknown( + icon = R.drawable.ic_vue_files_folder, + iconId = "ic_vue_files_folder", + ) + ) + + private var folderName = "" + private val folderIconId = MutableStateFlow(null) + + override val uiFlow: Flow = folderIconId.map { iconId -> + CreateAccFolderState( + icon = itemIconAct(ItemIconAct.Input(iconId, DefaultTo.Folder)) + ) + } + + + // region Event Handling + override suspend fun handleEvent(event: CreateAccFolderEvent) = when (event) { + is CreateAccFolderEvent.CreateFolder -> handleCreateFolder(event) + is CreateAccFolderEvent.NameChange -> handleFolderNameChange(event) + is CreateAccFolderEvent.IconChange -> handleIconChange(event) + } + + private suspend fun handleCreateFolder(event: CreateAccFolderEvent.CreateFolder) { + val newFolder = Folder( + id = UUID.randomUUID().toString(), + name = folderName, + icon = folderIconId.value, + color = event.color.toArgb(), + orderNum = newAccountTabItemOrderNumAct(Unit), + ) + writeAccountFolderAct(Modify.save(newFolder)) + writeAccountFolderContentAct( + WriteAccountFolderContentAct.Input( + folderId = newFolder.id, + accountIds = event.accounts.map { it.id } + ) + ) + } + + private fun handleFolderNameChange(event: CreateAccFolderEvent.NameChange) { + folderName = event.name + } + + private fun handleIconChange(event: CreateAccFolderEvent.IconChange) { + folderIconId.value = event.iconId + } + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderEvent.kt new file mode 100644 index 0000000000..74cdad9b7f --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderEvent.kt @@ -0,0 +1,21 @@ +package com.ivy.core.ui.account.folder.edit + +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.data.ItemIconId + +internal sealed interface EditAccFolderEvent { + data class Initial(val folderId: String) : EditAccFolderEvent + + object EditFolder : EditAccFolderEvent + + data class NameChange(val name: String) : EditAccFolderEvent + + data class IconChange(val iconId: ItemIconId) : EditAccFolderEvent + + data class ColorChange(val color: Color) : EditAccFolderEvent + + data class AccountsChange(val accounts: List) : EditAccFolderEvent + + object Delete : EditAccFolderEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderModal.kt new file mode 100644 index 0000000000..f9a15692e5 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderModal.kt @@ -0,0 +1,114 @@ +package com.ivy.core.ui.account.folder.edit + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.account.folder.BaseFolderModal +import com.ivy.core.ui.data.icon.dummyIconUnknown +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.l3_ivyComponents.modal.DeleteConfirmationModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.resources.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun BoxScope.EditAccFolderModal( + modal: IvyModal, + folderId: String, + level: Int = 1, +) { + val viewModel: EditAccFolderViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel = viewModel, preview = ::previewState) + + LaunchedEffect(folderId) { + viewModel?.onEvent(EditAccFolderEvent.Initial(folderId)) + } + + val deleteConfirmationModal = rememberIvyModal() + + val keyboardController = LocalSoftwareKeyboardController.current + BaseFolderModal( + modal = modal, + level = level, + autoFocusNameInput = false, + title = "Edit folder", + positiveButtonText = stringResource(R.string.save), + secondaryActions = { + DeleteButton { + keyboardController?.hide() + deleteConfirmationModal.show() + } + SpacerHor(width = 12.dp) + }, + initialName = state.initialName, + icon = state.icon, + color = state.color, + accounts = state.accounts, + onNameChane = { viewModel?.onEvent(EditAccFolderEvent.NameChange(it)) }, + onColorChange = { viewModel?.onEvent(EditAccFolderEvent.ColorChange(it)) }, + onIconChange = { viewModel?.onEvent(EditAccFolderEvent.IconChange(it)) }, + onAccountsChange = { viewModel?.onEvent(EditAccFolderEvent.AccountsChange(it)) }, + onSave = { + viewModel?.onEvent(EditAccFolderEvent.EditFolder) + } + ) + + DeleteConfirmationModal( + modal = deleteConfirmationModal, + level = level + 1, + ) { + modal.hide() + viewModel?.onEvent(EditAccFolderEvent.Delete) + } +} + +@Composable +private fun DeleteButton( + onClick: () -> Unit, +) { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Negative, + text = null, + icon = R.drawable.outline_delete_24, + onClick = onClick, + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + EditAccFolderModal( + modal = modal, + folderId = "", + ) + } +} + +private fun previewState() = EditAccFolderState( + icon = dummyIconUnknown(R.drawable.ic_vue_files_folder), + color = Purple, + initialName = "Folder 1", + accounts = listOf(), +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderState.kt new file mode 100644 index 0000000000..c75e3e0a44 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderState.kt @@ -0,0 +1,14 @@ +package com.ivy.core.ui.account.folder.edit + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.icon.ItemIcon + +@Immutable +internal data class EditAccFolderState( + val icon: ItemIcon, + val color: Color, + val initialName: String, + val accounts: List, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderViewModel.kt new file mode 100644 index 0000000000..926f823e2c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/edit/EditAccFolderViewModel.kt @@ -0,0 +1,125 @@ +package com.ivy.core.ui.account.folder.edit + +import androidx.compose.ui.graphics.toArgb +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.folder.AccountsInFolderAct +import com.ivy.core.domain.action.account.folder.FolderAct +import com.ivy.core.domain.action.account.folder.WriteAccountFolderAct +import com.ivy.core.domain.action.account.folder.WriteAccountFolderContentAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.ui.R +import com.ivy.core.ui.action.DefaultTo +import com.ivy.core.ui.action.ItemIconAct +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.ItemIconId +import com.ivy.data.account.Folder +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l0_system.color.toComposeColor +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +@HiltViewModel +internal class EditAccFolderViewModel @Inject constructor( + private val itemIconAct: ItemIconAct, + private val writeAccountFolderAct: WriteAccountFolderAct, + private val writeAccountFolderContentAct: WriteAccountFolderContentAct, + private val folderAct: FolderAct, + private val accountsInFolderAct: AccountsInFolderAct, + private val mapAccountUiAct: MapAccountUiAct, +) : SimpleFlowViewModel() { + override val initialUi = EditAccFolderState( + icon = ItemIcon.Unknown( + icon = R.drawable.ic_vue_files_folder, + iconId = "ic_vue_files_folder", + ), + color = Purple, + initialName = "", + accounts = emptyList(), + ) + + private var folder: Folder? = null + private var folderName = "" + private val initialName = MutableStateFlow(initialUi.initialName) + private val iconId = MutableStateFlow(null) + private val color = MutableStateFlow(initialUi.color) + private val accounts = MutableStateFlow(initialUi.accounts) + + override val uiFlow: Flow = combine( + initialName, iconId, color, accounts + ) { initialName, iconId, color, accounts -> + EditAccFolderState( + initialName = initialName, + icon = itemIconAct(ItemIconAct.Input(iconId, DefaultTo.Folder)), + color = color, + accounts = accounts + ) + } + + + // region Event Handling + override suspend fun handleEvent(event: EditAccFolderEvent) = when (event) { + is EditAccFolderEvent.Initial -> handleInitial(event) + is EditAccFolderEvent.EditFolder -> handleEditFolder() + is EditAccFolderEvent.NameChange -> handleFolderNameChange(event) + is EditAccFolderEvent.IconChange -> handleIconChange(event) + is EditAccFolderEvent.ColorChange -> handleColorChange(event) + is EditAccFolderEvent.AccountsChange -> handleAccountsChange(event) + EditAccFolderEvent.Delete -> handleDelete() + } + + private suspend fun handleInitial(event: EditAccFolderEvent.Initial) { + folderAct(event.folderId)?.let { folder -> + this.folder = folder + folderName = folder.name + initialName.value = folder.name + iconId.value = folder.icon + color.value = folder.color.toComposeColor() + accounts.value = accountsInFolderAct(folder.id) + .map { mapAccountUiAct(it) } + } + } + + private suspend fun handleEditFolder() { + val updated = folder?.copy( + name = folderName, + color = color.value.toArgb(), + icon = iconId.value + ) + if (updated != null) { + writeAccountFolderAct(Modify.save(updated)) + writeAccountFolderContentAct( + WriteAccountFolderContentAct.Input( + folderId = updated.id, + accountIds = accounts.value.map { it.id } + ) + ) + } + } + + private fun handleFolderNameChange(event: EditAccFolderEvent.NameChange) { + folderName = event.name + } + + private fun handleIconChange(event: EditAccFolderEvent.IconChange) { + iconId.value = event.iconId + } + + private fun handleColorChange(event: EditAccFolderEvent.ColorChange) { + color.value = event.color + } + + private fun handleAccountsChange(event: EditAccFolderEvent.AccountsChange) { + accounts.value = event.accounts + } + + private suspend fun handleDelete() { + folder?.let { + writeAccountFolderAct(Modify.delete(it.id)) + } + } + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerModal.kt new file mode 100644 index 0000000000..beff8a04f2 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerModal.kt @@ -0,0 +1,191 @@ +package com.ivy.core.ui.account.folder.pick + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.account.folder.create.CreateAccFolderModal +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.account.dummyFolderUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.* +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Negative +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.design.util.thenWhen + +@Composable +fun BoxScope.FolderPickerModal( + modal: IvyModal, + selected: FolderUi?, + level: Int = 1, + onPickFolder: (FolderUi?) -> Unit, +) { + val viewModel: FolderPickerViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value + ?: previewState() + + val createFolderModal = rememberIvyModal() + + Modal( + modal = modal, + level = level, + actions = { + Negative(text = "No folder") { + onPickFolder(null) + modal.hide() + } + } + ) { + LazyColumn { + item { + Title(text = "Choose folder") + } + folderItems( + folders = state.folders, + selected = selected, + onSelect = { + onPickFolder(it) + modal.hide() + } + ) + createFolderItem( + onCreateFolder = { createFolderModal.show() } + ) + item { + SpacerVer(height = 48.dp) // last item spacer + } + } + } + + CreateAccFolderModal( + modal = createFolderModal, + level = level + 1, + ) +} + +// region Folders +private fun LazyListScope.folderItems( + folders: List, + selected: FolderUi?, + onSelect: (FolderUi) -> Unit +) { + items( + items = folders, + key = { "folder_${it.id}" } + ) { folder -> + SpacerVer(height = 12.dp) + FolderItem( + folder = folder, + selected = folder.id == selected?.id + ) { + onSelect(folder) + } + } +} + +@Composable +internal fun FolderItem( + folder: FolderUi, + selected: Boolean, + onClick: () -> Unit +) { + val dynamicContrast = rememberDynamicContrast(folder.color) + val contrastColor = rememberContrast(folder.color) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(UI.shapes.squared) + .thenWhen { + when (selected) { + true -> background(folder.color, UI.shapes.squared) + .border(2.dp, dynamicContrast, UI.shapes.squared) + false -> border(2.dp, dynamicContrast, UI.shapes.squared) + } + } + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val color = if (selected) contrastColor else UI.colorsInverted.pure + ItemIcon( + itemIcon = folder.icon, + size = IconSize.S, + tint = color, + ) + SpacerHor(width = 8.dp) + B2(text = folder.name, color = color) + } +} +// endregion + +// region Add folder +fun LazyListScope.createFolderItem( + onCreateFolder: () -> Unit, +) { + item(key = "add_folder_item") { + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "New folder", + icon = R.drawable.ic_round_add_24, + onClick = onCreateFolder, + ) + } +} +// endregion + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + FolderPickerModal( + modal = modal, + selected = dummyFolderUi(id = "folder1"), + onPickFolder = {} + ) + } +} + +private fun previewState() = FolderPickerState( + folders = listOf( + dummyFolderUi(id = "folder1", name = "Folder 1", color = Green), + dummyFolderUi("Folder 2", color = Yellow), + dummyFolderUi("Folder 3", color = Purple), + ) +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerState.kt new file mode 100644 index 0000000000..5c10ebc03c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.account.folder.pick + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.account.FolderUi + +@Immutable +data class FolderPickerState( + val folders: List +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerViewModel.kt new file mode 100644 index 0000000000..7f1c4b29ed --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/folder/pick/FolderPickerViewModel.kt @@ -0,0 +1,31 @@ +package com.ivy.core.ui.account.folder.pick + +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.folder.AccountFoldersFlow +import com.ivy.core.domain.action.data.AccountListItem +import com.ivy.core.ui.action.mapping.account.MapFolderUiAct +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class FolderPickerViewModel @Inject constructor( + accountsFoldersFlow: AccountFoldersFlow, + private val mapFolderUiAct: MapFolderUiAct +) : SimpleFlowViewModel() { + override val initialUi = FolderPickerState(folders = emptyList()) + + override val uiFlow: Flow = + accountsFoldersFlow(Unit).map { accountsFolders -> + FolderPickerState( + folders = accountsFolders + .filterIsInstance() + .map { mapFolderUiAct(it.folder) } + ) + } + + // region Event Handling + override suspend fun handleEvent(event: Unit) {} + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerColumn.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerColumn.kt new file mode 100644 index 0000000000..5e275ff643 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerColumn.kt @@ -0,0 +1,127 @@ +package com.ivy.core.ui.account.pick + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.account.pick.component.SelectableAccountItem +import com.ivy.core.ui.account.pick.data.SelectableAccountUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.design.l0_system.color.* +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.WrapContentRow +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@Composable +fun ColumnScope.AccountPickerColumn( + selected: List, + deselectButton: Boolean, + modifier: Modifier = Modifier, + onAddAccount: (() -> Unit)?, + onSelectAccount: (AccountUi) -> Unit, + onDeselectAccount: (AccountUi) -> Unit, +) { + val viewModel: AccountPickerViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel = viewModel, preview = ::previewState) + + LaunchedEffect(selected) { + viewModel?.onEvent(AccountPickerEvent.SelectedChange(selected)) + } + + WrapContentRow( + modifier = modifier, + items = state.accounts, + itemKey = { it.account.id }, + horizontalMarginBetweenItems = 8.dp, + verticalMarginBetweenRows = 12.dp + ) { item -> + SelectableAccountItem( + item = item, + deselectButton = deselectButton, + onSelect = { onSelectAccount(item.account) }, + onDeselect = { onDeselectAccount(item.account) } + ) + } + if (onAddAccount != null) { + SpacerVer(height = 12.dp) + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = stringResource(R.string.add_account), + icon = R.drawable.ic_round_add_24, + onClick = onAddAccount + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + Column { + AccountPickerColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + selected = listOf(), + deselectButton = true, + onAddAccount = {}, + onSelectAccount = {}, + onDeselectAccount = {}, + ) + } + } +} + +@Preview +@Composable +private fun Preview_noDeselect() { + ComponentPreview { + Column { + AccountPickerColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + selected = listOf(), + deselectButton = false, + onAddAccount = {}, + onSelectAccount = {}, + onDeselectAccount = {}, + ) + } + } +} + + +private fun previewState() = AccountPickerState( + accounts = listOf( + dummyAccountUi("Account 1"), + dummyAccountUi("Account 2", color = Blue), + dummyAccountUi("DSK", color = Green), + dummyAccountUi("Unicredit Bulbank", color = Red), + dummyAccountUi("Revolut", color = Purple2Dark), + dummyAccountUi("Investments", color = Green2Dark), + dummyAccountUi("Cash", color = Green2Dark), + ).mapIndexed { index, acc -> + SelectableAccountUi(acc, selected = index % 3 == 0) + } +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerEvent.kt new file mode 100644 index 0000000000..9a54fb4395 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerEvent.kt @@ -0,0 +1,7 @@ +package com.ivy.core.ui.account.pick + +import com.ivy.core.ui.data.account.AccountUi + +sealed interface AccountPickerEvent { + data class SelectedChange(val selected: List) : AccountPickerEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerModal.kt new file mode 100644 index 0000000000..0eaff80287 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerModal.kt @@ -0,0 +1,88 @@ +package com.ivy.core.ui.account.pick + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.account.create.CreateAccountModal +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.AccountPickerModal( + modal: IvyModal, + level: Int = 1, + selected: List, + deselectButton: Boolean, + onSelectAccount: (AccountUi) -> Unit, + onDeselectAccount: (AccountUi) -> Unit, +) { + val createAccountModal = rememberIvyModal() + + Modal( + modal = modal, + level = level, + actions = { + // no actions + } + ) { + LazyColumn( + modifier = Modifier + ) { + item(key = "title") { + Title( + text = stringResource(id = R.string.account), + paddingStart = 24.dp, + ) + SpacerVer(height = 16.dp) + } + item(key = "accounts") { + AccountPickerColumn( + modifier = Modifier.padding(horizontal = 8.dp), + selected = selected, + deselectButton = deselectButton, + onAddAccount = { + createAccountModal.show() + }, + onSelectAccount = onSelectAccount, + onDeselectAccount = onDeselectAccount + ) + } + item(key = "last_item_spacer") { + SpacerVer(height = 24.dp) + } + } + } + + CreateAccountModal( + modal = createAccountModal, + level = level + 1, + ) +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + AccountPickerModal( + modal = modal, + selected = emptyList(), + deselectButton = false, + onSelectAccount = {}, + onDeselectAccount = {}, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerState.kt new file mode 100644 index 0000000000..7fe1dff4ef --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.account.pick + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.account.pick.data.SelectableAccountUi + +@Immutable +data class AccountPickerState( + val accounts: List, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerViewModel.kt new file mode 100644 index 0000000000..1eabc00462 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountPickerViewModel.kt @@ -0,0 +1,44 @@ +package com.ivy.core.ui.account.pick + +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountsFlow +import com.ivy.core.ui.account.pick.data.SelectableAccountUi +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.data.account.AccountState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +@HiltViewModel +class AccountPickerViewModel @Inject constructor( + accountsFlow: AccountsFlow, + private val mapAccountUiAct: MapAccountUiAct, +) : SimpleFlowViewModel() { + override val initialUi = AccountPickerState( + accounts = emptyList() + ) + + private val selectedIds = MutableStateFlow(listOf()) + + override val uiFlow: Flow = combine( + accountsFlow(), selectedIds + ) { accounts, selectedIds -> + AccountPickerState( + accounts = accounts.filter { it.state != AccountState.Archived } + .map { mapAccountUiAct(it) } + .map { SelectableAccountUi(it, selected = selectedIds.contains(it.id)) } + ) + } + + // region Event Handling + override suspend fun handleEvent(event: AccountPickerEvent) = when (event) { + is AccountPickerEvent.SelectedChange -> handleSelectedChange(event) + } + + private fun handleSelectedChange(event: AccountPickerEvent.SelectedChange) { + selectedIds.value = event.selected.map { it.id } + } + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountsState.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountsState.kt new file mode 100644 index 0000000000..8289d8f9ef --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountsState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.account.pick + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.account.AccountUi + +@Immutable +data class AccountsState( + val accounts: List +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountsViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountsViewModel.kt new file mode 100644 index 0000000000..2bbd44172a --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/AccountsViewModel.kt @@ -0,0 +1,32 @@ +package com.ivy.core.ui.account.pick + +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountsFlow +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class AccountsViewModel @Inject constructor( + accountsFlow: AccountsFlow, + private val mapAccountUiAct: MapAccountUiAct, +) : SimpleFlowViewModel() { + override val initialUi = AccountsState( + accounts = emptyList(), + ) + + override val uiFlow: Flow = accountsFlow() + .map { accounts -> + AccountsState( + accounts = accounts.map { + mapAccountUiAct(it) + }, + ) + } + + + override suspend fun handleEvent(event: Unit) { + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/SingleAccountPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/SingleAccountPickerModal.kt new file mode 100644 index 0000000000..a8bfc6a81e --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/SingleAccountPickerModal.kt @@ -0,0 +1,55 @@ +package com.ivy.core.ui.account.pick + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.core.ui.account.create.CreateAccountModal +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.SingleAccountPickerModal( + modal: IvyModal, + level: Int = 1, + selected: AccountUi, + onSelectAccount: (AccountUi) -> Unit, +) { + val createAccountModal = rememberIvyModal() + + AccountPickerModal( + modal = modal, + level = level, + selected = listOf(selected), + deselectButton = false, + onSelectAccount = { + onSelectAccount(it) + modal.hide() + }, + onDeselectAccount = { + // do nothing + } + ) + + CreateAccountModal( + modal = createAccountModal, + level = level + 1, + ) +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + SingleAccountPickerModal( + modal = modal, + selected = dummyAccountUi(), + onSelectAccount = {} + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/SingleAccountPickerRow.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/SingleAccountPickerRow.kt new file mode 100644 index 0000000000..e9bcfd7e1a --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/SingleAccountPickerRow.kt @@ -0,0 +1,136 @@ +package com.ivy.core.ui.account.pick + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.account.pick.component.SelectableAccountItem +import com.ivy.core.ui.account.pick.data.SelectableAccountUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.lastItemSpacerHorizontal +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.design.l0_system.color.* +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@Composable +fun SingleAccountPickerRow( + modifier: Modifier = Modifier, + selected: AccountUi, + onAddAccount: () -> Unit, + onSelectedChange: (AccountUi) -> Unit, +) { + val viewModel: AccountsViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel = viewModel, preview = ::previewState) + + val listState = rememberLazyListState() + + LaunchedEffect(selected, state.accounts) { + state.accounts.indexOfFirst { it.id == selected.id } + .let { index -> + if (index != -1) { + listState.animateScrollToItem(index) + } + } + } + + LazyRow( + modifier = modifier.fillMaxWidth(), + state = listState, + verticalAlignment = Alignment.CenterVertically, + ) { + accountItems( + items = state.accounts, + selected = selected, + onSelectedChange = onSelectedChange, + ) + addAccount(onAddAccount) + lastItemSpacerHorizontal(width = 12.dp) + } +} + +private fun LazyListScope.accountItems( + items: List, + selected: AccountUi, + onSelectedChange: (AccountUi) -> Unit, +) { + items( + items = items, + key = { it.id } + ) { item -> + SpacerHor(width = 8.dp) + SelectableAccountItem( + item = SelectableAccountUi( + account = item, + selected = item.id == selected.id, + ), + deselectButton = false, + onSelect = { onSelectedChange(item) }, + onDeselect = { + // do nothing because we always want to have a selected account + } + ) + } +} + +private fun LazyListScope.addAccount( + onClick: () -> Unit +) { + item(key = "add_account") { + SpacerHor(width = 8.dp) + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = stringResource(R.string.add_account), + icon = R.drawable.ic_round_add_24, + onClick = onClick + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + SingleAccountPickerRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + selected = dummyAccountUi(), + onAddAccount = {}, + onSelectedChange = {}, + ) + } +} + +private fun previewState() = AccountsState( + accounts = listOf( + dummyAccountUi("Account 1"), + dummyAccountUi("Account 2", color = Blue), + dummyAccountUi("DSK", color = Green), + dummyAccountUi("Unicredit Bulbank", color = Red), + dummyAccountUi("Revolut", color = Purple2Dark), + dummyAccountUi("Investments", color = Green2Dark), + dummyAccountUi("Cash", color = Green2Dark), + ) +) +// endregion diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/component/SelectableAccountItem.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/component/SelectableAccountItem.kt new file mode 100644 index 0000000000..f63eed3928 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/component/SelectableAccountItem.kt @@ -0,0 +1,43 @@ +package com.ivy.core.ui.account.pick.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.core.ui.account.pick.data.SelectableAccountUi +import com.ivy.core.ui.component.SelectableItem +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.design.util.ComponentPreview + +@Composable +fun SelectableAccountItem( + item: SelectableAccountUi, + deselectButton: Boolean, + onSelect: () -> Unit, + onDeselect: () -> Unit, +) { + SelectableItem( + name = item.account.name, + icon = item.account.icon, + color = item.account.color, + selected = item.selected, + deselectButton = deselectButton, + onSelect = onSelect, + onDeselect = onDeselect, + ) +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + SelectableAccountItem( + item = SelectableAccountUi( + account = dummyAccountUi(), + selected = true, + ), + deselectButton = true, + onSelect = {}, + onDeselect = {}, + ) + } +} diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/pick/data/SelectableAccountUi.kt b/core/ui/src/main/java/com/ivy/core/ui/account/pick/data/SelectableAccountUi.kt new file mode 100644 index 0000000000..3595746133 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/pick/data/SelectableAccountUi.kt @@ -0,0 +1,10 @@ +package com.ivy.core.ui.account.pick.data + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.account.AccountUi + +@Immutable +data class SelectableAccountUi( + val account: AccountUi, + val selected: Boolean +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsEvent.kt new file mode 100644 index 0000000000..6848c15504 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsEvent.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.account.reorder + +import com.ivy.core.ui.account.reorder.data.ReorderAccListItemUi + +sealed interface ReorderAccountsEvent { + data class Reorder( + val reordered: List + ) : ReorderAccountsEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsModal.kt b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsModal.kt new file mode 100644 index 0000000000..1b3dc291d7 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsModal.kt @@ -0,0 +1,133 @@ +package com.ivy.core.ui.account.reorder + +import ReorderModal +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.account.reorder.data.ReorderAccListItemUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.account.dummyFolderUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.* +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.DividerW +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@Composable +fun BoxScope.ReorderAccountsModal( + modal: IvyModal, + level: Int = 1, +) { + val viewModel: ReorderAccountsViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel, preview = ::previewState) + + ReorderModal( + modal = modal, + level = level, + items = state.items, + onReorder = { + viewModel?.onEvent(ReorderAccountsEvent.Reorder(it)) + } + ) { _, item -> + Item(item = item) + } +} + +@Composable +private fun RowScope.Item(item: ReorderAccListItemUi) { + when (item) { + is ReorderAccListItemUi.AccountHolder -> AccountCard(account = item.account) + is ReorderAccListItemUi.FolderHolder -> FolderCard(folder = item.folder) + ReorderAccListItemUi.FolderEnd -> FolderEnd() + } +} + +@Composable +private fun AccountCard(account: AccountUi) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) // margin top + .padding(start = 8.dp, end = 16.dp) + .background(account.color, UI.shapes.rounded) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val contrast = rememberContrast(account.color) + ItemIcon(itemIcon = account.icon, size = IconSize.S, tint = contrast) + SpacerHor(width = 4.dp) + B2(text = account.name, color = contrast) + } +} + +@Composable +private fun FolderCard(folder: FolderUi) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) // margin top + .padding(start = 8.dp, end = 16.dp) + .background(folder.color, UI.shapes.squared) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val contrast = rememberContrast(folder.color) + ItemIcon(itemIcon = folder.icon, size = IconSize.S, tint = contrast) + SpacerHor(width = 4.dp) + B2(text = folder.name, color = contrast) + } +} + +@Composable +private fun RowScope.FolderEnd() { + SpacerHor(width = 8.dp) + DividerW() + SpacerHor(width = 8.dp) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + ReorderAccountsModal(modal = modal) + } +} + +private fun previewState() = ReorderAccountsStateUi( + items = listOf( + dummyAccountHolder("Account 1", color = Green), + dummyAccountHolder("Account 2", color = Purple), + dummyFolderHolder("Folder 1", color = Green2Dark), + dummyAccountHolder("Account 3", color = Red2), + dummyAccountHolder("Account 4", color = YellowDark), + dummyFolderHolder("Folder 2", color = Green), + dummyAccountHolder("Account 5", color = Blue), + dummyFolderHolder("Folder 3", color = Green), + ), +) + +private fun dummyAccountHolder(name: String, color: Color) = ReorderAccListItemUi.AccountHolder( + dummyAccountUi(name = name, color = color), +) + +private fun dummyFolderHolder(name: String, color: Color) = ReorderAccListItemUi.FolderHolder( + dummyFolderUi(name = name, color = color) +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsStateUi.kt b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsStateUi.kt new file mode 100644 index 0000000000..c8bcd80024 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsStateUi.kt @@ -0,0 +1,7 @@ +package com.ivy.core.ui.account.reorder + +import com.ivy.core.ui.account.reorder.data.ReorderAccListItemUi + +data class ReorderAccountsStateUi( + val items: List +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsViewModel.kt new file mode 100644 index 0000000000..7dc2561a9d --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/ReorderAccountsViewModel.kt @@ -0,0 +1,130 @@ +package com.ivy.core.ui.account.reorder + +import arrow.core.Either +import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.action.account.WriteAccountsAct +import com.ivy.core.domain.action.account.folder.AccountFoldersFlow +import com.ivy.core.domain.action.account.folder.WriteAccountFolderAct +import com.ivy.core.domain.action.data.AccountListItem +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.ui.account.reorder.data.ReorderAccListItemUi +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.core.ui.action.mapping.account.MapFolderUiAct +import com.ivy.data.account.Account +import com.ivy.data.account.Folder +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import com.ivy.core.ui.account.reorder.ReorderAccountsViewModel.InternalState as InternalState1 + +@HiltViewModel +internal class ReorderAccountsViewModel @Inject constructor( + accountFoldersFlow: AccountFoldersFlow, + private val mapAccountUiAct: MapAccountUiAct, + private val mapFolderUiAct: MapFolderUiAct, + private val writeAccountFolderAct: WriteAccountFolderAct, + private val writeAccountsAct: WriteAccountsAct, +) : FlowViewModel() { + override val initialState = InternalState( + items = emptyList(), + ) + + override val initialUi = ReorderAccountsStateUi( + items = emptyList(), + ) + + override val stateFlow: Flow = accountFoldersFlow(Unit).map { items -> + InternalState( + items = items, + ) + } + + override val uiFlow: Flow = stateFlow + .map { internalState -> + internalState.items.flatMap { + when (it) { + is AccountListItem.AccountHolder -> listOf( + ReorderAccListItemUi.AccountHolder( + account = mapAccountUiAct(it.account) + ) + ) + is AccountListItem.Archived -> it.accounts.toReorderAccListItems() + is AccountListItem.FolderWithAccounts -> listOf( + ReorderAccListItemUi.FolderHolder( + folder = mapFolderUiAct(it.folder) + ) + ) + it.accounts.toReorderAccListItems() + listOf( + ReorderAccListItemUi.FolderEnd + ) + } + } + }.map { items -> + ReorderAccountsStateUi( + items = items, + ) + } + + private suspend fun List.toReorderAccListItems(): List = + this.map { + ReorderAccListItemUi.AccountHolder( + account = mapAccountUiAct(it) + ) + } + + // region Event handling + override suspend fun handleEvent(event: ReorderAccountsEvent) = when (event) { + is ReorderAccountsEvent.Reorder -> handleReorder(event) + } + + private suspend fun handleReorder(event: ReorderAccountsEvent.Reorder) { + val internalItems = state.value.items + val accounts = internalItems.flatMap { + when (it) { + is AccountListItem.AccountHolder -> listOf(it.account) + is AccountListItem.Archived -> it.accounts + is AccountListItem.FolderWithAccounts -> it.accounts + } + } + val folders = internalItems.filterIsInstance() + .map { it.folder } + + val reordered = event.reordered.mapIndexedNotNull { index, item -> + // TODO: Optimize this expensive search logic with a map + when (item) { + is ReorderAccListItemUi.AccountHolder -> + accounts.firstOrNull { it.id.toString() == item.account.id } + ?.copy(orderNum = index.toDouble()) + ?.let { Either.Left(it) } + is ReorderAccListItemUi.FolderHolder -> + folders.firstOrNull { it.id == item.folder.id } + ?.copy(orderNum = index.toDouble()) + ?.let { Either.Right(it) } + ReorderAccListItemUi.FolderEnd -> null + } + } + + val expectedCount = uiState.value.items.count { + when (it) { + is ReorderAccListItemUi.AccountHolder -> true + is ReorderAccListItemUi.FolderHolder -> true + ReorderAccListItemUi.FolderEnd -> false + } + } + // verify no lost of data + if (reordered.size == expectedCount) { + val accountsToUpdate = reordered.filterIsInstance>() + .map { it.value } + writeAccountsAct(Modify.saveMany(accountsToUpdate)) + val foldersToUpdate = reordered.filterIsInstance>() + .map { it.value } + writeAccountFolderAct(Modify.saveMany(foldersToUpdate)) + } + } + + // endregion + + data class InternalState( + val items: List, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/account/reorder/data/ReorderAccListItemUi.kt b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/data/ReorderAccListItemUi.kt new file mode 100644 index 0000000000..c27cc6105d --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/account/reorder/data/ReorderAccListItemUi.kt @@ -0,0 +1,17 @@ +package com.ivy.core.ui.account.reorder.data + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.FolderUi + +@Immutable +sealed interface ReorderAccListItemUi { + @Immutable + data class AccountHolder(val account: AccountUi) : ReorderAccListItemUi + + @Immutable + data class FolderHolder(val folder: FolderUi) : ReorderAccListItemUi + + @Immutable + object FolderEnd : ReorderAccListItemUi +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/action/BaseCurrencyRepresentationFlow.kt b/core/ui/src/main/java/com/ivy/core/ui/action/BaseCurrencyRepresentationFlow.kt new file mode 100644 index 0000000000..29c163eee7 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/action/BaseCurrencyRepresentationFlow.kt @@ -0,0 +1,35 @@ +package com.ivy.core.ui.action + +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.exchange.ExchangeFlow +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.format +import com.ivy.data.Value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class BaseCurrencyRepresentationFlow @Inject constructor( + private val baseCurrencyFlow: BaseCurrencyFlow, + private val exchangeFlow: ExchangeFlow, +) : FlowAction() { + @OptIn(ExperimentalCoroutinesApi::class) + override fun Value.createFlow(): Flow = + baseCurrencyFlow().map { baseCurrency -> + if (currency != baseCurrency) { + exchangeFlow(ExchangeFlow.Input(this, baseCurrency)) + } else { + flowOf(null) + } + }.flatMapLatest { flow -> + flow.map { value -> + value?.let { + format(it, shortenFiat = true) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/action/ItemIconAct.kt b/core/ui/src/main/java/com/ivy/core/ui/action/ItemIconAct.kt index 9a49a428fe..7fcda4d72f 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/action/ItemIconAct.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/action/ItemIconAct.kt @@ -1,9 +1,9 @@ package com.ivy.core.ui.action -import com.ivy.base.R import com.ivy.core.domain.action.Action import com.ivy.core.ui.data.icon.ItemIcon import com.ivy.data.ItemIconId +import com.ivy.resources.R import javax.inject.Inject class ItemIconAct @Inject constructor( @@ -15,21 +15,30 @@ class ItemIconAct @Inject constructor( ) override suspend fun Input.willDo(): ItemIcon { - fun Input.default(): ItemIcon = ItemIcon.Sized( - iconS = when (defaultTo) { - DefaultTo.Account -> R.drawable.ic_custom_account_s - DefaultTo.Category -> R.drawable.ic_custom_category_s - }, - iconM = when (defaultTo) { - DefaultTo.Account -> R.drawable.ic_custom_account_m - DefaultTo.Category -> R.drawable.ic_custom_category_m - }, - iconL = when (defaultTo) { - DefaultTo.Account -> R.drawable.ic_custom_account_l - DefaultTo.Category -> R.drawable.ic_custom_category_l - }, - iconId = iconId - ) + fun Input.default(): ItemIcon = when (defaultTo) { + DefaultTo.Folder -> ItemIcon.Unknown( + icon = R.drawable.ic_vue_files_folder, + iconId = "ic_vue_files_folder", + ) + else -> ItemIcon.Sized( + iconS = when (defaultTo) { + DefaultTo.Account -> R.drawable.ic_custom_account_s + DefaultTo.Category -> R.drawable.ic_custom_category_s + else -> error("not expected size icon") + }, + iconM = when (defaultTo) { + DefaultTo.Account -> R.drawable.ic_custom_account_m + DefaultTo.Category -> R.drawable.ic_custom_category_m + else -> error("not expected size icon") + }, + iconL = when (defaultTo) { + DefaultTo.Account -> R.drawable.ic_custom_account_l + DefaultTo.Category -> R.drawable.ic_custom_category_l + else -> error("not expected size icon") + }, + iconId = iconId + ) + } return iconId?.let { itemIconOptionalAct(it) } ?: default() } @@ -37,5 +46,6 @@ class ItemIconAct @Inject constructor( enum class DefaultTo { Account, - Category + Category, + Folder } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapCategoryUiAct.kt b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapCategoryUiAct.kt index fa7f115ec8..bcab84c202 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapCategoryUiAct.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapCategoryUiAct.kt @@ -14,6 +14,7 @@ class MapCategoryUiAct @Inject constructor( id = domain.id.toString(), name = domain.name, icon = itemIconAct(ItemIconAct.Input(iconId = domain.icon, defaultTo = DefaultTo.Category)), - color = domain.color.toComposeColor() + color = domain.color.toComposeColor(), + hasParent = domain.parentCategoryId != null, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapAccountUiAct.kt b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/account/MapAccountUiAct.kt similarity index 71% rename from core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapAccountUiAct.kt rename to core/ui/src/main/java/com/ivy/core/ui/action/mapping/account/MapAccountUiAct.kt index 88f5d65f81..fc74d37f5f 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapAccountUiAct.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/account/MapAccountUiAct.kt @@ -1,8 +1,9 @@ -package com.ivy.core.ui.action.mapping +package com.ivy.core.ui.action.mapping.account import com.ivy.core.ui.action.DefaultTo import com.ivy.core.ui.action.ItemIconAct -import com.ivy.core.ui.data.AccountUi +import com.ivy.core.ui.action.mapping.MapUiAction +import com.ivy.core.ui.data.account.AccountUi import com.ivy.data.account.Account import com.ivy.design.l0_system.color.toComposeColor import javax.inject.Inject @@ -14,6 +15,7 @@ class MapAccountUiAct @Inject constructor( id = domain.id.toString(), name = domain.name, icon = itemIconAct(ItemIconAct.Input(iconId = domain.icon, defaultTo = DefaultTo.Account)), - color = domain.color.toComposeColor() + color = domain.color.toComposeColor(), + excluded = domain.excluded, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/account/MapFolderUiAct.kt b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/account/MapFolderUiAct.kt new file mode 100644 index 0000000000..926f04e4f6 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/account/MapFolderUiAct.kt @@ -0,0 +1,21 @@ +package com.ivy.core.ui.action.mapping.account + +import com.ivy.core.ui.action.DefaultTo +import com.ivy.core.ui.action.ItemIconAct +import com.ivy.core.ui.action.mapping.MapUiAction +import com.ivy.core.ui.data.account.FolderUi +import com.ivy.data.account.Folder +import com.ivy.design.l0_system.color.toComposeColor +import javax.inject.Inject + +class MapFolderUiAct @Inject constructor( + private val itemIconAct: ItemIconAct, +) : MapUiAction() { + override suspend fun transform(domain: Folder) = FolderUi( + id = domain.id, + name = domain.name, + icon = itemIconAct(ItemIconAct.Input(domain.icon, DefaultTo.Folder)), + color = domain.color.toComposeColor(), + orderNum = domain.orderNum, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapTransactionListUiAct.kt b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/trn/MapTransactionListUiAct.kt similarity index 73% rename from core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapTransactionListUiAct.kt rename to core/ui/src/main/java/com/ivy/core/ui/action/mapping/trn/MapTransactionListUiAct.kt index 4ea1fe047e..cd0ba8184c 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/MapTransactionListUiAct.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/trn/MapTransactionListUiAct.kt @@ -1,25 +1,30 @@ -package com.ivy.core.ui.action.mapping +package com.ivy.core.ui.action.mapping.trn import android.content.Context -import com.ivy.common.time.dateNowLocal import com.ivy.common.time.format -import com.ivy.common.time.timeNow +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.domain.pure.format.format import com.ivy.core.ui.R +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.action.mapping.MapUiAction +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct import com.ivy.core.ui.data.transaction.* -import com.ivy.core.ui.time.formatNicely import com.ivy.data.Value -import com.ivy.data.transaction.* +import com.ivy.data.transaction.DueSection +import com.ivy.data.transaction.Transaction +import com.ivy.data.transaction.TransactionsList +import com.ivy.data.transaction.TrnListItem import dagger.hilt.android.qualifiers.ApplicationContext -import java.time.LocalDateTime import javax.inject.Inject class MapTransactionListUiAct @Inject constructor( private val mapAccountUiAct: MapAccountUiAct, private val mapCategoryUiAct: MapCategoryUiAct, + private val mapTrnTimeUiAct: MapTrnTimeUiAct, @ApplicationContext - private val appContext: Context + private val appContext: Context, + private val timeProvider: TimeProvider ) : MapUiAction() { override suspend fun transform(domain: TransactionsList): TransactionsListUi = TransactionsListUi( @@ -47,7 +52,7 @@ class MapTransactionListUiAct @Inject constructor( dueType = dueType, income = formatNonZero(value = domain.income), expense = formatNonZero(value = domain.expense), - trns = domain.trns.map { mapTransaction(it) } + trns = domain.trns.map { mapTrnListItem(it) } ) } } @@ -56,7 +61,7 @@ class MapTransactionListUiAct @Inject constructor( is TrnListItem.DateDivider -> mapDateDivider(domain) is TrnListItem.Transfer -> TrnListItemUi.Transfer( batchId = domain.batchId, - time = mapTrnTimeUi(domain.time), + time = mapTrnTimeUiAct(domain.time), from = mapTransaction(domain.from), to = mapTransaction(domain.to), fee = domain.fee?.let { mapTransaction(it) } @@ -67,9 +72,10 @@ class MapTransactionListUiAct @Inject constructor( private fun mapDateDivider( domain: TrnListItem.DateDivider ): TrnListItemUi.DateDivider { - val today = dateNowLocal() + val today = timeProvider.dateNow() return TrnListItemUi.DateDivider( + id = domain.id, date = domain.date.format( if (today.year == domain.date.year) "MMMM dd." else "MMM dd. yyyy" ), @@ -80,7 +86,8 @@ class MapTransactionListUiAct @Inject constructor( else -> null } ?: today.format("EEEE"), cashflow = format(value = domain.cashflow, shortenFiat = true), - positiveCashflow = domain.cashflow.amount > 0 + positiveCashflow = domain.cashflow.amount > 0, + collapsed = domain.collapsed, ) } @@ -90,25 +97,8 @@ class MapTransactionListUiAct @Inject constructor( value = format(value = domain.value, shortenFiat = false), account = mapAccountUiAct(domain.account), category = domain.category?.let { mapCategoryUiAct(it) }, - time = mapTrnTimeUi(domain.time), + time = mapTrnTimeUiAct(domain.time), title = domain.title, description = domain.description, ) - - private fun mapTrnTimeUi(domain: TrnTime): TrnTimeUi { - fun formatTime(time: LocalDateTime): String = - time.formatNicely(context = appContext, includeWeekDay = true) - - return when (domain) { - is TrnTime.Actual -> TrnTimeUi.Actual( - actual = formatTime(domain.actual).uppercase() - ) - is TrnTime.Due -> TrnTimeUi.Due( - dueOn = appContext.getString( - R.string.due_on, formatTime(domain.due) - ).uppercase(), - upcoming = timeNow().isBefore(domain.due) - ) - } - } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/action/mapping/trn/MapTrnTimeUiAct.kt b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/trn/MapTrnTimeUiAct.kt new file mode 100644 index 0000000000..83e8c87486 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/action/mapping/trn/MapTrnTimeUiAct.kt @@ -0,0 +1,44 @@ +package com.ivy.core.ui.action.mapping.trn + +import android.content.Context +import com.ivy.common.time.deviceFormat +import com.ivy.common.time.provider.TimeProvider +import com.ivy.core.ui.R +import com.ivy.core.ui.action.mapping.MapUiAction +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.core.ui.time.formatNicely +import com.ivy.data.transaction.TrnTime +import dagger.hilt.android.qualifiers.ApplicationContext +import java.time.LocalDateTime +import javax.inject.Inject + +class MapTrnTimeUiAct @Inject constructor( + @ApplicationContext + private val appContext: Context, + private val timeProvider: TimeProvider +) : MapUiAction() { + override suspend fun transform(domain: TrnTime): TrnTimeUi = mapTrnTimeUi(domain) + + private fun mapTrnTimeUi(domain: TrnTime): TrnTimeUi { + fun formatDateTime(time: LocalDateTime): String = + time.formatNicely( + context = appContext, + timeProvider = timeProvider, + includeWeekDay = true + ) + + return when (domain) { + is TrnTime.Actual -> TrnTimeUi.Actual( + actualDate = formatDateTime(domain.actual).uppercase(), + actualTime = domain.actual.toLocalTime().deviceFormat(appContext), + ) + is TrnTime.Due -> TrnTimeUi.Due( + dueOnDate = appContext.getString( + R.string.due_on, formatDateTime(domain.due) + ).uppercase(), + dueOnTime = domain.due.toLocalTime().deviceFormat(appContext), + upcoming = timeProvider.timeNow().isBefore(domain.due) + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModal.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModal.kt new file mode 100644 index 0000000000..9069bb9acf --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModal.kt @@ -0,0 +1,146 @@ +package com.ivy.core.ui.amount + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.amount.components.AmountSection +import com.ivy.core.ui.amount.components.Keyboard +import com.ivy.core.ui.amount.data.CalculatorResultUi +import com.ivy.core.ui.currency.CurrencyPickerModal +import com.ivy.data.Value +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Secondary +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l2_components.modal.scope.ModalActionsScope +import com.ivy.design.l2_components.modal.scope.ModalScope +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.resources.R + +/** + * @param key used to refresh the initial amount when the key changes + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun BoxScope.AmountModal( + modal: IvyModal, + initialAmount: Value?, + level: Int = 1, + calculatorVisible: MutableState = remember { mutableStateOf(false) }, + key: String? = null, + contentAbove: (@Composable ModalScope.() -> Unit)? = { + SpacerVer(height = 24.dp) + }, + moreActions: (@Composable ModalActionsScope.() -> Unit)? = null, + onAmountEnter: (Value) -> Unit, +) { + val viewModel: AmountModalViewModel? = hiltViewModelPreviewSafe(key = key) + val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() + + LaunchedEffect(initialAmount, key) { + viewModel?.onEvent(AmountModalEvent.Initial(initialAmount)) + } + + val currencyPickerModal = rememberIvyModal() + + Modal( + modal = modal, + level = level, + contentModifier = Modifier.verticalScroll(rememberScrollState()), + actions = { + moreActions?.invoke(this) + Secondary( + text = null, + icon = R.drawable.ic_vue_edu_calculator, + feeling = if (calculatorVisible.value) + Feeling.Negative else Feeling.Positive, + hapticFeedback = true + ) { + calculatorVisible.value = !calculatorVisible.value + if (!calculatorVisible.value) { + viewModel?.onEvent(AmountModalEvent.CalculatorEquals) + } + } + SpacerHor(width = 8.dp) + Positive( + text = stringResource(R.string.enter), + icon = R.drawable.ic_round_check_24 + ) { + state.amount?.let(onAmountEnter) + modal.hide() + } + } + ) { + // Close the software keyboard if it's open + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(Unit) { + keyboardController?.hide() + } + + contentAbove?.invoke(this) + AmountSection( + calculatorVisible = calculatorVisible.value, + expression = state.expression, + currency = state.currency, + amountInBaseCurrency = state.amountBaseCurrency, + calculatorTempResult = state.calculatorResult, + onPickCurrency = { currencyPickerModal.show() } + ) + SpacerVer(height = 12.dp) + Keyboard( + calculatorVisible = calculatorVisible.value, + onCalculatorEvent = { viewModel?.onEvent(AmountModalEvent.CalculatorOperator(it)) }, + onNumberEvent = { viewModel?.onEvent(AmountModalEvent.Number(it)) }, + onDecimalSeparator = { viewModel?.onEvent(AmountModalEvent.DecimalSeparator) }, + onBackspace = { viewModel?.onEvent(AmountModalEvent.Backspace) }, + onCalculatorC = { viewModel?.onEvent(AmountModalEvent.CalculatorC) }, + onCalculatorEquals = { viewModel?.onEvent(AmountModalEvent.CalculatorEquals) } + ) + SpacerVer(height = 16.dp) + } + + CurrencyPickerModal( + modal = currencyPickerModal, + level = 2, + initialCurrency = state.currency, + onCurrencyPick = { viewModel?.onEvent(AmountModalEvent.CurrencyChange(it)) } + ) +} + + +// region Previews +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + AmountModal( + modal = modal, + initialAmount = Value(0.0, "USD"), + onAmountEnter = {} + ) + } +} + +private fun previewState() = AmountModalState( + expression = "500.00", + currency = "USD", + amount = null, + amountBaseCurrency = ValueUi("1,032.55", "BGN"), + calculatorResult = CalculatorResultUi(result = "", isError = true) +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalEvent.kt new file mode 100644 index 0000000000..a9e027b808 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalEvent.kt @@ -0,0 +1,17 @@ +package com.ivy.core.ui.amount + +import com.ivy.data.CurrencyCode +import com.ivy.data.Value + +sealed interface AmountModalEvent { + data class Number(val number: Int) : AmountModalEvent + data class CalculatorOperator(val operator: com.ivy.math.calculator.CalculatorOperator) : + AmountModalEvent + object DecimalSeparator : AmountModalEvent + object Backspace : AmountModalEvent + object CalculatorEquals : AmountModalEvent + object CalculatorC : AmountModalEvent + + data class CurrencyChange(val currency: CurrencyCode) : AmountModalEvent + data class Initial(val initialAmount: Value?) : AmountModalEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalState.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalState.kt new file mode 100644 index 0000000000..ab437aae64 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalState.kt @@ -0,0 +1,16 @@ +package com.ivy.core.ui.amount + +import androidx.compose.runtime.Immutable +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.amount.data.CalculatorResultUi +import com.ivy.data.CurrencyCode +import com.ivy.data.Value + +@Immutable +internal data class AmountModalState( + val expression: String?, + val currency: CurrencyCode, + val amount: Value?, + val amountBaseCurrency: ValueUi?, + val calculatorResult: CalculatorResultUi?, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalViewModel.kt new file mode 100644 index 0000000000..64076f1524 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/AmountModalViewModel.kt @@ -0,0 +1,203 @@ +package com.ivy.core.ui.amount + +import com.ivy.common.isNotBlank +import com.ivy.common.isNotEmpty +import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.action.exchange.ExchangeRatesFlow +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow +import com.ivy.core.domain.pure.exchange.exchange +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.format +import com.ivy.core.ui.amount.data.CalculatorResultUi +import com.ivy.data.Value +import com.ivy.data.exchange.ExchangeRatesData +import com.ivy.math.calculator.appendDecimalSeparator +import com.ivy.math.calculator.appendTo +import com.ivy.math.calculator.beautify +import com.ivy.math.calculator.hasObviousResult +import com.ivy.math.evaluate +import com.ivy.math.formatNumber +import com.ivy.math.localDecimalSeparator +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +internal class AmountModalViewModel @Inject constructor( + private val exchangeRatesFlow: ExchangeRatesFlow, + private val baseCurrencyFlow: BaseCurrencyFlow, +) : FlowViewModel() { + override val initialState = InternalState( + exchangeData = ExchangeRatesData(baseCurrency = "", rates = emptyMap()), + ) + + override val initialUi = AmountModalState( + expression = null, + currency = "", + amount = null, + amountBaseCurrency = null, + calculatorResult = CalculatorResultUi(result = "", isError = true) + ) + + // region Local state + private var overrideExpressionForInitial = false + + private val expression = MutableStateFlow("") + private val currency = MutableStateFlow("") + private val showExpressionError = MutableStateFlow(false) + // endregion + + // regin Flows + override val stateFlow: Flow = exchangeRatesFlow().map { + InternalState(exchangeData = it) + } + + override val uiFlow: Flow = combine( + expression, currency, calculateFlow(), amountBaseCurrencyFlow() + ) { expression, currency, (calcResult, expressionValue), amountBaseCurrency -> + AmountModalState( + expression = beautify(expression), + currency = currency, + amount = expressionValue?.let { Value(it, currency) }, + amountBaseCurrency = amountBaseCurrency, + calculatorResult = calcResult.takeIf { + calcResult.isError || !hasObviousResult(expression, expressionValue) + } + ) + } + + private fun amountBaseCurrencyFlow(): Flow = combine( + currency, baseCurrencyFlow(), calculateFlow(), exchangeRatesFlow() + ) { currency, baseCurrency, calcResult, rates -> + if (currency == baseCurrency) null else calcResult.second?.let { + exchange( + rates, + from = currency, + to = baseCurrency, + amount = it + ).orNull().takeIf { exchangedAmount -> + exchangedAmount != null && exchangedAmount > 0.0 + } + }?.let { exchangedAmount -> + format( + Value(amount = exchangedAmount, currency = baseCurrency), + shortenFiat = true + ) + } + } + + private fun calculateFlow(): Flow> = combine( + expression, showExpressionError + ) { expression, showExpressionError -> + val evaluated = evaluate(expression) + CalculatorResultUi( + result = evaluated?.let(::formatNumber) ?: "Error", + isError = evaluated == null && showExpressionError + ) to evaluated + } + // endregion + + + // region Event Handling + override suspend fun handleEvent(event: AmountModalEvent) = when (event) { + AmountModalEvent.Backspace -> handleBackspace() + AmountModalEvent.DecimalSeparator -> handleDecimalSeparator() + is AmountModalEvent.CalculatorOperator -> handleCalculatorOperator(event) + is AmountModalEvent.Number -> handleNumber(event) + is AmountModalEvent.CurrencyChange -> handleCurrencyChange(event) + is AmountModalEvent.Initial -> handleInitial(event) + AmountModalEvent.CalculatorC -> handleCalculatorC() + AmountModalEvent.CalculatorEquals -> handleCalculatorEquals() + } + + private fun handleBackspace() { + if (expression.value.isNotEmpty()) { + expression.value = expression.value.dropLast(1) + overrideExpressionForInitial = false // expression is not initial, disable override + } + } + + private fun handleDecimalSeparator() { + expression.value = appendDecimalSeparator( + expression = expression.value, + decimalSeparator = localDecimalSeparator(), + ) + overrideExpressionForInitial = false // expression is not initial, disable override + } + + // region Calculator + private fun handleCalculatorOperator(event: AmountModalEvent.CalculatorOperator) { + expression.value = appendTo(expression = expression.value, operator = event.operator) + overrideExpressionForInitial = false // expression is not initial, disable override + } + + private fun handleCalculatorC() { + expression.value = "" + overrideExpressionForInitial = false // expression is not initial, disable override + } + + private fun handleCalculatorEquals() { + val evaluated = evaluate(expression.value) + if (evaluated != null) { + expression.value = format(Value(evaluated, currency.value), shortenFiat = false).amount + } else if (expression.value.isNotBlank()) { + showExpressionError.value = true + } + overrideExpressionForInitial = false // expression is not initial, disable override + } + // endregion + + private fun handleNumber(event: AmountModalEvent.Number) { + // for better UX, allow the user to override the initial expression + val currentExpression = if (overrideExpressionForInitial) "" else expression.value + expression.value = appendTo(currentExpression, digit = event.number) + showExpressionError.value = false // remove shown error + + overrideExpressionForInitial = false // expression is not initial, disable override + } + + private suspend fun handleCurrencyChange(event: AmountModalEvent.CurrencyChange) { + val currency = currency.value + val newCurrency = event.currency + + val enteredValue = uiState.value.amount + if (newCurrency != currency && enteredValue != null) { + // Converted the entered amount to the new currency + val exchangeData = state.value.exchangeData + Timber.d("exchangeData = $exchangeData") + exchange( + exchangeData = exchangeData, + from = currency, to = newCurrency, + amount = enteredValue.amount + ).orNull()?.let { exchangedAmount -> + expression.value = format( + Value(exchangedAmount, newCurrency), shortenFiat = false + ).amount + } + } + + // update the currency in the UI + this.currency.value = newCurrency + + overrideExpressionForInitial = false // expression is not initial, disable override + } + + private fun handleInitial(event: AmountModalEvent.Initial) { + event.initialAmount?.let { initial -> + currency.value = initial.currency + if (initial.amount != 0.0) { + expression.value = format(initial, shortenFiat = false).amount + overrideExpressionForInitial = true + } + } + } + // endregion + + data class InternalState( + val exchangeData: ExchangeRatesData + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/components/AmountSection.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/components/AmountSection.kt new file mode 100644 index 0000000000..3ca02e4e8b --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/components/AmountSection.kt @@ -0,0 +1,204 @@ +package com.ivy.core.ui.amount.components + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.amount.data.CalculatorResultUi +import com.ivy.core.ui.amount.util.rememberDecimalSeparator +import com.ivy.data.CurrencyCode +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.style +import com.ivy.design.l1_buildingBlocks.B1Second +import com.ivy.design.l1_buildingBlocks.H2Second +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.data.none +import com.ivy.design.l2_components.button.Btn +import com.ivy.design.l2_components.button.TextIcon +import com.ivy.design.util.ComponentPreview +import com.ivy.resources.R + +@Composable +internal fun ColumnScope.AmountSection( + calculatorVisible: Boolean, + expression: String?, + currency: CurrencyCode, + amountInBaseCurrency: ValueUi?, + calculatorTempResult: CalculatorResultUi?, + onPickCurrency: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + H2Second( + text = expression ?: "0${rememberDecimalSeparator()}00", + fontWeight = FontWeight.Bold, + color = if (expression != null) + UI.colorsInverted.pure else UI.colors.neutral + ) + AnimatedVisibility( + visible = !calculatorVisible && currency.isNotBlank(), + enter = expandHorizontally() + fadeIn(), + exit = shrinkHorizontally() + fadeOut(), + ) { + CurrencyPicker(currency = currency, onClick = onPickCurrency) + } + } + AnimatedAmountInBaseCurrency( + calculatorVisible = calculatorVisible, + amountInBaseCurrency = amountInBaseCurrency, + ) + CalculatorTemporaryResult( + calculatorVisible = calculatorVisible, + result = calculatorTempResult, + ) +} + +// region Currency Picker +@Composable +private fun CurrencyPicker( + currency: CurrencyCode, + onClick: () -> Unit +) { + Btn.TextIcon( + modifier = Modifier.padding(start = 8.dp), + text = currency, + iconRight = R.drawable.round_expand_more_24, + iconPadding = 4.dp, + background = none(), + textStyle = UI.typoSecond.h2.style( + color = UI.colors.primary, + fontWeight = FontWeight.ExtraBold + ), + iconTint = UI.colors.primary, + onClick = onClick + ) +} +// endregion + +// region Amount in base currency +@Composable +private fun ColumnScope.AnimatedAmountInBaseCurrency( + calculatorVisible: Boolean, + amountInBaseCurrency: ValueUi? +) { + AnimatedVisibility( + modifier = Modifier.align(Alignment.CenterHorizontally), + visible = !calculatorVisible && amountInBaseCurrency != null, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Row( + modifier = Modifier + .padding(top = 0.dp) + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + B1Second( + text = amountInBaseCurrency?.amount ?: "", + fontWeight = FontWeight.Normal + ) + SpacerHor(width = 4.dp) + B1Second( + text = amountInBaseCurrency?.currency ?: "", + fontWeight = FontWeight.Bold + ) + } + } +} +// endregion + +// region Calculator temp result +@Composable +private fun ColumnScope.CalculatorTemporaryResult( + calculatorVisible: Boolean, + result: CalculatorResultUi?, +) { + AnimatedVisibility( + modifier = Modifier.align(Alignment.CenterHorizontally), + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + visible = calculatorVisible && result != null + ) { + if (result != null) { + B1Second( + text = result.result, + fontWeight = FontWeight.ExtraBold, + color = if (result.isError) UI.colors.red else UI.colors.primary + ) + } + } +} +// endregion + + +// region Previews +@Preview +@Composable +private fun Preview() { + ComponentPreview { + Column { + AmountSection( + calculatorVisible = false, + expression = null, + currency = "USD", + amountInBaseCurrency = ValueUi( + amount = "10.00", + currency = "BGN" + ), + calculatorTempResult = null, + onPickCurrency = {} + ) + } + } +} + +@Preview +@Composable +private fun Preview_Calculator() { + ComponentPreview { + Column { + AmountSection( + calculatorVisible = true, + expression = "5+5", + currency = "USD", + amountInBaseCurrency = null, + calculatorTempResult = CalculatorResultUi( + result = "10.00", + isError = false + ), + onPickCurrency = {} + ) + } + } +} + +@Preview +@Composable +private fun Preview_Calculator_error() { + ComponentPreview { + Column { + AmountSection( + calculatorVisible = true, + expression = "5+", + currency = "USD", + amountInBaseCurrency = null, + calculatorTempResult = CalculatorResultUi( + result = "Error", + isError = true + ), + onPickCurrency = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/components/Keyboard.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/components/Keyboard.kt new file mode 100644 index 0000000000..45d4e1f5c1 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/components/Keyboard.kt @@ -0,0 +1,352 @@ +package com.ivy.core.ui.amount.components + +import androidx.compose.animation.* +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.amount.util.rememberDecimalSeparator +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.* +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.toColor +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.thenWhen +import com.ivy.math.calculator.CalculatorOperator +import com.ivy.resources.R + +// region Customize UI +private const val keypadOuterWeight = 1f +private const val keypadInnerWeight = 0.15f +private val keypadButtonBig = 90.dp +private val keypadButtonSmall = 82.dp +private val keyboardVerticalMargin = 4.dp +// endregion + +@Suppress("unused") +@Composable +internal fun ColumnScope.Keyboard( + calculatorVisible: Boolean, + onCalculatorEvent: (CalculatorOperator) -> Unit, + onNumberEvent: (Int) -> Unit, + onDecimalSeparator: () -> Unit, + onBackspace: () -> Unit, + onCalculatorC: () -> Unit, + onCalculatorEquals: () -> Unit, +) { + val keypadBtnSize by animateDpAsState( + targetValue = if (calculatorVisible) + keypadButtonSmall else keypadButtonBig + ) + CalculatorTopRow( + calculatorVisible = calculatorVisible, + keypadBtnSize = keypadBtnSize, + onCalculatorEvent = onCalculatorEvent, + onCalculatorC = onCalculatorC, + ) + // margin is built-in in calculator's top row + KeyboardRow { + SpacerWeight(weight = keypadOuterWeight) + KeypadButton(symbol = "7", size = keypadBtnSize, onClick = { onNumberEvent(7) }) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton(symbol = "8", size = keypadBtnSize, onClick = { onNumberEvent(8) }) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton(symbol = "9", size = keypadBtnSize, onClick = { onNumberEvent(9) }) + AnimatedCalculatorButton( + calculatorVisible = calculatorVisible, + symbol = "*", + onClick = { onCalculatorEvent(CalculatorOperator.Multiply) } + ) + SpacerWeight(weight = keypadOuterWeight) + } + SpacerVer(height = keyboardVerticalMargin) + KeyboardRow { + SpacerWeight(weight = keypadOuterWeight) + KeypadButton(symbol = "4", size = keypadBtnSize, onClick = { onNumberEvent(4) }) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton(symbol = "5", size = keypadBtnSize, onClick = { onNumberEvent(5) }) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton(symbol = "6", size = keypadBtnSize, onClick = { onNumberEvent(6) }) + AnimatedCalculatorButton( + calculatorVisible = calculatorVisible, + symbol = "-", + onClick = { onCalculatorEvent(CalculatorOperator.Minus) } + ) + SpacerWeight(weight = keypadOuterWeight) + } + SpacerVer(height = keyboardVerticalMargin) + KeyboardRow { + SpacerWeight(weight = keypadOuterWeight) + KeypadButton(symbol = "1", size = keypadBtnSize, onClick = { onNumberEvent(1) }) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton(symbol = "2", size = keypadBtnSize, onClick = { onNumberEvent(2) }) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton(symbol = "3", size = keypadBtnSize, onClick = { onNumberEvent(3) }) + AnimatedCalculatorButton( + calculatorVisible = calculatorVisible, + symbol = "+", + onClick = { onCalculatorEvent(CalculatorOperator.Plus) } + ) + SpacerWeight(weight = keypadOuterWeight) + } + SpacerVer(height = keyboardVerticalMargin) + KeyboardRow { + SpacerWeight(weight = keypadOuterWeight) + KeypadButton( + symbol = rememberDecimalSeparator().toString(), + size = keypadBtnSize, + onClick = onDecimalSeparator + ) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton(symbol = "0", size = keypadBtnSize, onClick = { onNumberEvent(0) }) + SpacerWeight(weight = keypadInnerWeight) + BackSpaceButton( + size = keypadBtnSize, + onClick = onBackspace, + onLongClick = onCalculatorC + ) + AnimatedCalculatorButton( + calculatorVisible = calculatorVisible, + symbol = "=", + feeling = Feeling.Positive, + onClick = onCalculatorEquals, + ) + SpacerWeight(weight = keypadOuterWeight) + } +} + +// region Calculator +@Composable +private fun CalculatorTopRow( + calculatorVisible: Boolean, + keypadBtnSize: Dp, + onCalculatorEvent: (CalculatorOperator) -> Unit, + onCalculatorC: () -> Unit, +) { + AnimatedVisibility( + visible = calculatorVisible, + enter = expandHorizontally() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + KeyboardRow( + modifier = Modifier.padding(bottom = keyboardVerticalMargin) + ) { + SpacerWeight(weight = keypadOuterWeight) + KeypadButton( + symbol = "C", + size = keypadBtnSize, + visibility = Visibility.High, + feeling = Feeling.Negative, + onClick = onCalculatorC, + ) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton( + symbol = "( )", + visibility = Visibility.High, + feeling = Feeling.Positive, + size = keypadBtnSize, + onClick = { onCalculatorEvent(CalculatorOperator.Brackets) } + ) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton( + symbol = "%", + visibility = Visibility.High, + feeling = Feeling.Positive, + size = keypadBtnSize, + onClick = { onCalculatorEvent(CalculatorOperator.Percent) } + ) + SpacerWeight(weight = keypadInnerWeight) + KeypadButton( + symbol = "/", + size = keypadBtnSize, + visibility = Visibility.High, + feeling = Feeling.Positive, + onClick = { onCalculatorEvent(CalculatorOperator.Divide) } + ) + SpacerWeight(weight = keypadOuterWeight) + } + } +} + +@Composable +private fun RowScope.AnimatedCalculatorButton( + calculatorVisible: Boolean, + symbol: String, + modifier: Modifier = Modifier, + feeling: Feeling = Feeling.Positive, + onClick: () -> Unit +) { + if (calculatorVisible) { + SpacerWeight(weight = keypadInnerWeight) + } + AnimatedVisibility( + visible = calculatorVisible, + enter = expandHorizontally() + fadeIn(), + exit = shrinkHorizontally() + fadeOut(), + ) { + KeypadButton( + modifier = modifier, + feeling = feeling, + visibility = Visibility.High, + size = keypadButtonSmall, + symbol = symbol, + onClick = onClick + ) + } +} +// endregion + +@Composable +private fun KeyboardRow( + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} + +// region Keypad Buttons +@Composable +private fun KeypadButton( + symbol: String, + size: Dp, + modifier: Modifier = Modifier, + visibility: Visibility = Visibility.Medium, + feeling: Feeling = Feeling.Positive, + onClick: () -> Unit +) { + KeypadButtonBox( + modifier = modifier, + feeling = feeling, + visibility = visibility, + size = size, + onClick = onClick + ) { + B1Second( + text = symbol, + color = when (visibility) { + Visibility.Focused, + Visibility.High -> + rememberContrast(feeling.toColor()) + Visibility.Medium, + Visibility.Low -> UI.colorsInverted.pure + }, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +private fun BackSpaceButton( + size: Dp, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { + KeypadButtonBox( + modifier = modifier, + feeling = Feeling.Negative, + size = size, + onClick = onClick, + onLongClick = onLongClick + ) { + IconRes( + icon = R.drawable.outline_backspace_24, + tint = UI.colorsInverted.pure, + ) + } +} + +@Composable +private fun KeypadButtonBox( + feeling: Feeling, + size: Dp, + modifier: Modifier = Modifier, + visibility: Visibility = Visibility.Medium, + onClick: () -> Unit, + onLongClick: (() -> Unit)? = null, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier + .size(size) + .clip(UI.shapes.circle) + .hapticClickable(onClick = onClick, onLongClick = onLongClick) + .padding(all = 4.dp) + .thenWhen { + when (visibility) { + Visibility.Focused, + Visibility.High -> background( + color = feeling.toColor(), + shape = UI.shapes.circle + ) + Visibility.Low, + Visibility.Medium -> border( + width = 1.dp, + color = feeling.toColor(), + shape = UI.shapes.circle + ) + } + }, + contentAlignment = Alignment.Center, + content = content + ) +} +// endregion + + +// region Previews +@Preview +@Composable +private fun Preview() { + ComponentPreview { + Column { + Keyboard( + calculatorVisible = false, + onCalculatorEvent = {}, + onNumberEvent = {}, + onDecimalSeparator = {}, + onBackspace = {}, + onCalculatorC = {}, + onCalculatorEquals = {} + ) + } + } +} + +@Preview +@Composable +private fun Preview_calculator_visible() { + ComponentPreview { + Column { + Keyboard( + calculatorVisible = false, + onCalculatorEvent = {}, + onNumberEvent = {}, + onDecimalSeparator = {}, + onBackspace = {}, + onCalculatorC = {}, + onCalculatorEquals = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/data/CalculatorResultUi.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/data/CalculatorResultUi.kt new file mode 100644 index 0000000000..2afe9b4bf7 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/data/CalculatorResultUi.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.amount.data + +import androidx.compose.runtime.Immutable + +@Immutable +data class CalculatorResultUi( + val result: String, + val isError: Boolean +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/amount/util/LocalSeparators.kt b/core/ui/src/main/java/com/ivy/core/ui/amount/util/LocalSeparators.kt new file mode 100644 index 0000000000..155abc1f90 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/amount/util/LocalSeparators.kt @@ -0,0 +1,8 @@ +package com.ivy.core.ui.amount.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.ivy.math.localDecimalSeparator + +@Composable +fun rememberDecimalSeparator(): Char = remember { localDecimalSeparator() } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/BaseCategoryModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/BaseCategoryModal.kt new file mode 100644 index 0000000000..709d59f0cf --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/BaseCategoryModal.kt @@ -0,0 +1,203 @@ +package com.ivy.core.ui.category + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.category.component.CategoryTypeSection +import com.ivy.core.ui.category.component.ParentCategoryButton +import com.ivy.core.ui.category.pickparent.ParentCategoryPickerModal +import com.ivy.core.ui.color.ColorButton +import com.ivy.core.ui.color.picker.ColorPickerModal +import com.ivy.core.ui.component.ItemIconNameRow +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.data.icon.dummyIconSized +import com.ivy.core.ui.icon.picker.IconPickerModal +import com.ivy.data.ItemIconId +import com.ivy.data.category.CategoryType +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.DividerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l2_components.modal.scope.ModalActionsScope +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.util.IvyPreview + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun BoxScope.BaseCategoryModal( + modal: IvyModal, + level: Int, + autoFocusNameInput: Boolean, + title: String, + nameInputHint: String, + positiveActionText: String, + icon: ItemIcon, + initialName: String, + color: Color, + parent: CategoryUi?, + type: CategoryType, + secondaryActions: (@Composable ModalActionsScope.() -> Unit)? = null, + contentBelow: (LazyListScope.() -> Unit)? = null, + onIconChange: (ItemIconId) -> Unit, + onNameChange: (String) -> Unit, + onColorChange: (Color) -> Unit, + onParentCategoryChange: (CategoryUi?) -> Unit, + onTypeChange: (CategoryType) -> Unit, + onSave: (SaveCategoryInfo) -> Unit, +) { + val iconPickerModal = rememberIvyModal() + val colorPickerModal = rememberIvyModal() + val chooseParentModal = rememberIvyModal() + + val keyboardController = LocalSoftwareKeyboardController.current + Modal( + modal = modal, + level = level, + actions = { + secondaryActions?.invoke(this) + Positive( + text = positiveActionText, + feeling = Feeling.Custom(color) + ) { + onSave( + SaveCategoryInfo( + color = color, + parent = parent, + ) + ) + keyboardController?.hide() + modal.hide() + } + } + ) { + LazyColumn(modifier = Modifier.weight(1f)) { + item(key = "modal_title") { + Title(text = title) + SpacerVer(height = 24.dp) + } + item(key = "icon_name_color") { + // Keep in one item because so the title + // won't disappear on scroll + ItemIconNameRow( + icon = icon, + color = color, + initialName = initialName, + nameInputHint = nameInputHint, + autoFocusInput = autoFocusNameInput, + onPickIcon = { + keyboardController?.hide() + iconPickerModal.show() + }, + onNameChange = onNameChange, + ) + SpacerVer(height = 16.dp) + ColorButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = color + ) { + keyboardController?.hide() + colorPickerModal.show() + } + SpacerVer(height = 16.dp) + } + item(key = "parent_category") { + ParentCategoryButton( + parent = parent, + color = color, + ) { + keyboardController?.hide() + chooseParentModal.show() + } + } + item(key = "line_divider") { + SpacerVer(height = 24.dp) + DividerHor() + SpacerVer(height = 12.dp) + } + item(key = "category_type") { + CategoryTypeSection( + type = type, + onSelect = onTypeChange + ) + } + contentBelow?.invoke(this) + item(key = "last_item_spacer") { + SpacerVer(height = 48.dp) // last spacer + } + } + } + + IconPickerModal( + modal = iconPickerModal, + level = level + 1, + initialIcon = icon, + color = color, + onIconPick = onIconChange, + ) + ColorPickerModal( + modal = colorPickerModal, + level = level + 1, + initialColor = color, + onColorPicked = onColorChange, + ) + ParentCategoryPickerModal( + modal = chooseParentModal, + level = level + 1, + selected = parent, + onPick = onParentCategoryChange + ) +} + +data class SaveCategoryInfo( + val color: Color, + val parent: CategoryUi? +) + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + BaseCategoryModal( + modal = modal, + level = 1, + autoFocusNameInput = false, + title = stringResource(R.string.edit_category), + nameInputHint = stringResource(R.string.category_name), + positiveActionText = stringResource(R.string.save), + icon = dummyIconSized(R.drawable.ic_custom_category_m), + color = UI.colors.primary, + initialName = "Category", + parent = null, + type = CategoryType.Both, + onNameChange = {}, + onIconChange = {}, + onSave = {}, + onColorChange = {}, + onParentCategoryChange = {}, + onTypeChange = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/CategoryCard.kt b/core/ui/src/main/java/com/ivy/core/ui/category/CategoryCard.kt deleted file mode 100644 index 354196cc45..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/category/CategoryCard.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.ivy.core.ui.category - -// The card from "Categories" screen \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/CategoryModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/CategoryModal.kt deleted file mode 100644 index 9229a3758e..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/category/CategoryModal.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.ivy.core.ui.category - diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/component/CategoryTypeSection.kt b/core/ui/src/main/java/com/ivy/core/ui/category/component/CategoryTypeSection.kt new file mode 100644 index 0000000000..0025c383b8 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/component/CategoryTypeSection.kt @@ -0,0 +1,106 @@ +package com.ivy.core.ui.category.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.data.category.CategoryType +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + + +@Composable +fun CategoryTypeSection( + type: CategoryType, + onSelect: (CategoryType) -> Unit +) { + B1( + modifier = Modifier.padding(start = 24.dp), + text = "Category type" + ) + SpacerVer(height = 8.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CategoryTypeButton( + modifier = Modifier.weight(1f), + type = CategoryType.Income, + selected = type == CategoryType.Income, + onSelect = onSelect + ) + SpacerHor(width = 8.dp) + CategoryTypeButton( + modifier = Modifier.weight(1f), + type = CategoryType.Expense, + selected = type == CategoryType.Expense, + onSelect = onSelect + ) + SpacerHor(width = 8.dp) + CategoryTypeButton( + modifier = Modifier.weight(1f), + type = CategoryType.Both, + selected = type == CategoryType.Both, + onSelect = onSelect + ) + } +} + +@Composable +private fun CategoryTypeButton( + type: CategoryType, + selected: Boolean, + modifier: Modifier = Modifier, + onSelect: (CategoryType) -> Unit +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = if (selected) Visibility.High else Visibility.Medium, + feeling = if (selected) Feeling.Custom( + when (type) { + CategoryType.Income -> UI.colors.green + CategoryType.Expense -> UI.colors.red + CategoryType.Both -> UI.colors.primary + } + ) else Feeling.Neutral, + text = when (type) { + CategoryType.Expense -> "Expense" + CategoryType.Income -> "Income" + CategoryType.Both -> "Both" + }, + icon = null + ) { + onSelect(type) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + Column { + CategoryTypeSection( + type = CategoryType.Both, + onSelect = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/component/ParentCategoryButton.kt b/core/ui/src/main/java/com/ivy/core/ui/category/component/ParentCategoryButton.kt new file mode 100644 index 0000000000..c8029cea6e --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/component/ParentCategoryButton.kt @@ -0,0 +1,98 @@ +package com.ivy.core.ui.category.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +@Composable +internal fun ColumnScope.ParentCategoryButton( + parent: CategoryUi?, + modifier: Modifier = Modifier, + color: Color, + onClick: () -> Unit +) { + B1( + modifier = Modifier.padding(start = 24.dp), + text = "Parent category" + ) + SpacerVer(height = 8.dp) + if (parent != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(UI.shapes.rounded) + .background(parent.color) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + val contrast = rememberContrast(color = parent.color) + ItemIcon(itemIcon = parent.icon, size = IconSize.S, tint = contrast) + SpacerHor(width = 12.dp) + B1(text = parent.name, color = contrast) + } + } else { + IvyButton( + modifier = modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Custom(color), + text = "Choose parent", + icon = R.drawable.ic_custom_category_s, + onClick = onClick + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview_None() { + ComponentPreview { + Column { + ParentCategoryButton( + parent = null, + color = Purple, + onClick = {} + ) + } + } +} + +@Preview +@Composable +private fun Preview_Selected() { + ComponentPreview { + Column { + ParentCategoryButton( + parent = dummyCategoryUi("Parent"), + color = Purple, + onClick = {} + ) + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryEvent.kt new file mode 100644 index 0000000000..5d483247af --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryEvent.kt @@ -0,0 +1,19 @@ +package com.ivy.core.ui.category.create + +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.CategoryUi +import com.ivy.data.ItemIconId +import com.ivy.data.category.CategoryType + +internal sealed interface CreateCategoryEvent { + data class CreateCategory( + val color: Color, + val parent: CategoryUi? + ) : CreateCategoryEvent + + data class IconChange(val iconId: ItemIconId) : CreateCategoryEvent + + data class NameChange(val name: String) : CreateCategoryEvent + + data class CategoryTypeChange(val categoryType: CategoryType) : CreateCategoryEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryModal.kt new file mode 100644 index 0000000000..ba06f5872e --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryModal.kt @@ -0,0 +1,75 @@ +package com.ivy.core.ui.category.create + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.* +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.core.ui.R +import com.ivy.core.ui.category.BaseCategoryModal +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.icon.dummyIconSized +import com.ivy.data.category.CategoryType +import com.ivy.design.l0_system.UI +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@Composable +fun BoxScope.CreateCategoryModal( + modal: IvyModal, + level: Int = 1 +) { + val viewModel: CreateCategoryViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() + + val primary = UI.colors.primary + var color by remember(primary) { mutableStateOf(primary) } + var parent by remember { mutableStateOf(null) } + var type by remember { mutableStateOf(CategoryType.Both) } + + val newCategoryText = "New Category" + BaseCategoryModal( + modal = modal, + level = level, + autoFocusNameInput = true, + title = newCategoryText, + nameInputHint = newCategoryText, + positiveActionText = stringResource(R.string.add_category), + icon = state.icon, + initialName = "", + color = color, + parent = parent, + type = type, + onNameChange = { viewModel?.onEvent(CreateCategoryEvent.NameChange(it)) }, + onIconChange = { viewModel?.onEvent(CreateCategoryEvent.IconChange(it)) }, + onParentCategoryChange = { parent = it }, + onTypeChange = { type = it }, + onColorChange = { color = it }, + onSave = { + viewModel?.onEvent( + CreateCategoryEvent.CreateCategory( + color = it.color, + parent = it.parent + ) + ) + } + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + CreateCategoryModal(modal = modal) + } +} + +private fun previewState() = CreateCategoryState( + icon = dummyIconSized(R.drawable.ic_custom_category_m) +) +// endregion diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryState.kt b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryState.kt new file mode 100644 index 0000000000..f700c91cb2 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.category.create + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.icon.ItemIcon + +@Immutable +internal data class CreateCategoryState( + val icon: ItemIcon +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryViewModel.kt new file mode 100644 index 0000000000..16f5c658bf --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/create/CreateCategoryViewModel.kt @@ -0,0 +1,85 @@ +package com.ivy.core.ui.category.create + +import androidx.compose.ui.graphics.toArgb +import com.ivy.common.toUUID +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.category.NewCategoryOrderNumAct +import com.ivy.core.domain.action.category.WriteCategoriesAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.ui.R +import com.ivy.core.ui.action.DefaultTo +import com.ivy.core.ui.action.ItemIconAct +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.ItemIconId +import com.ivy.data.SyncState +import com.ivy.data.category.Category +import com.ivy.data.category.CategoryState +import com.ivy.data.category.CategoryType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import java.util.* +import javax.inject.Inject + +@HiltViewModel +internal class CreateCategoryViewModel @Inject constructor( + private val itemIconAct: ItemIconAct, + private val writeCategoriesAct: WriteCategoriesAct, + private val newCategoryOrderNumAct: NewCategoryOrderNumAct +) : SimpleFlowViewModel() { + override val initialUi = CreateCategoryState( + icon = ItemIcon.Sized( + iconS = R.drawable.ic_custom_category_s, + iconM = R.drawable.ic_custom_category_m, + iconL = R.drawable.ic_custom_category_l, + iconId = null + ) + ) + + private var name = "" + private val iconId = MutableStateFlow(null) + private val categoryType = MutableStateFlow(CategoryType.Both) + + override val uiFlow: Flow = iconId.map { iconId -> + CreateCategoryState( + icon = itemIconAct(ItemIconAct.Input(iconId, DefaultTo.Account)) + ) + } + + // region Event Handling + override suspend fun handleEvent(event: CreateCategoryEvent) = when (event) { + is CreateCategoryEvent.CreateCategory -> createCategory(event) + is CreateCategoryEvent.IconChange -> handleIconPick(event) + is CreateCategoryEvent.NameChange -> handleNameChange(event) + is CreateCategoryEvent.CategoryTypeChange -> handleCategoryTypeChange(event) + } + + private suspend fun createCategory(event: CreateCategoryEvent.CreateCategory) { + val new = Category( + id = UUID.randomUUID(), + name = name, + color = event.color.toArgb(), + icon = iconId.value, + parentCategoryId = event.parent?.id?.toUUID(), + orderNum = newCategoryOrderNumAct(Unit), + state = CategoryState.Default, + type = categoryType.value, + sync = SyncState.Syncing, + ) + writeCategoriesAct(Modify.save(new)) + } + + private fun handleIconPick(event: CreateCategoryEvent.IconChange) { + iconId.value = event.iconId + } + + private fun handleNameChange(event: CreateCategoryEvent.NameChange) { + name = event.name + } + + private fun handleCategoryTypeChange(event: CreateCategoryEvent.CategoryTypeChange) { + categoryType.value = event.categoryType + } + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryEvent.kt new file mode 100644 index 0000000000..5d2285cf4b --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryEvent.kt @@ -0,0 +1,26 @@ +package com.ivy.core.ui.category.edit + +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.CategoryUi +import com.ivy.data.ItemIconId +import com.ivy.data.category.CategoryType + +internal sealed interface EditCategoryEvent { + data class Initial(val categoryId: String) : EditCategoryEvent + + object EditCategory : EditCategoryEvent + + data class IconChange(val iconId: ItemIconId) : EditCategoryEvent + + data class NameChange(val name: String) : EditCategoryEvent + + data class ColorChange(val color: Color) : EditCategoryEvent + + data class ParentChange(val parent: CategoryUi?) : EditCategoryEvent + + data class TypeChange(val type: CategoryType) : EditCategoryEvent + + object Archive : EditCategoryEvent + object Unarchive : EditCategoryEvent + object Delete : EditCategoryEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryModal.kt new file mode 100644 index 0000000000..5c2d7eb0aa --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryModal.kt @@ -0,0 +1,126 @@ +package com.ivy.core.ui.category.edit + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.category.BaseCategoryModal +import com.ivy.core.ui.category.edit.component.DeleteCategoryModal +import com.ivy.core.ui.data.icon.dummyIconSized +import com.ivy.data.category.CategoryType +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.button.ArchiveButton +import com.ivy.design.l3_ivyComponents.button.DeleteButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun BoxScope.EditCategoryModal( + modal: IvyModal, + categoryId: String, + level: Int = 1, +) { + val viewModel: EditCategoryViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() + + LaunchedEffect(categoryId) { + viewModel?.onEvent(EditCategoryEvent.Initial(categoryId)) + } + + val deleteModal = rememberIvyModal() + + val keyboardController = LocalSoftwareKeyboardController.current + BaseCategoryModal( + modal = modal, + level = level, + autoFocusNameInput = false, + title = stringResource(R.string.edit_category), + nameInputHint = stringResource(R.string.category_name), + positiveActionText = stringResource(R.string.save), + secondaryActions = { + ArchiveButton( + archived = state.archived, + color = state.color, + onArchive = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditCategoryEvent.Archive) + }, + onUnarchive = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditCategoryEvent.Unarchive) + } + ) + SpacerHor(width = 8.dp) + DeleteButton { + keyboardController?.hide() + deleteModal.show() + } + SpacerHor(width = 12.dp) + }, + icon = state.icon, + initialName = state.initialName, + color = state.color, + parent = state.parent, + type = state.type, + onNameChange = { viewModel?.onEvent(EditCategoryEvent.NameChange(it)) }, + onIconChange = { viewModel?.onEvent(EditCategoryEvent.IconChange(it)) }, + onColorChange = { viewModel?.onEvent(EditCategoryEvent.ColorChange(it)) }, + onTypeChange = { viewModel?.onEvent(EditCategoryEvent.TypeChange(it)) }, + onParentCategoryChange = { viewModel?.onEvent(EditCategoryEvent.ParentChange(it)) }, + onSave = { viewModel?.onEvent(EditCategoryEvent.EditCategory) } + ) + + DeleteCategoryModal( + modal = deleteModal, + level = level + 1, + categoryName = state.initialName, + archived = state.archived, + onArchive = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditCategoryEvent.Archive) + }, + onDelete = { + keyboardController?.hide() + modal.hide() + viewModel?.onEvent(EditCategoryEvent.Delete) + } + ) +} + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + EditCategoryModal( + modal = modal, + categoryId = "" + ) + } +} + +private fun previewState() = EditCategoryState( + categoryId = "", + icon = dummyIconSized(R.drawable.ic_custom_category_m), + initialName = "Category", + parent = null, + color = Purple, + archived = false, + type = CategoryType.Both, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryState.kt b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryState.kt new file mode 100644 index 0000000000..beb7c93c29 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryState.kt @@ -0,0 +1,18 @@ +package com.ivy.core.ui.category.edit + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.category.CategoryType + +@Immutable +internal data class EditCategoryState( + val categoryId: String, + val icon: ItemIcon, + val color: Color, + val initialName: String, + val parent: CategoryUi?, + val archived: Boolean, + val type: CategoryType, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryViewModel.kt new file mode 100644 index 0000000000..9e40750374 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/edit/EditCategoryViewModel.kt @@ -0,0 +1,202 @@ +package com.ivy.core.ui.category.edit + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.Toast +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.ivy.common.toUUID +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.category.CategoriesFlow +import com.ivy.core.domain.action.category.CategoryByIdAct +import com.ivy.core.domain.action.category.WriteCategoriesAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.ui.R +import com.ivy.core.ui.action.DefaultTo +import com.ivy.core.ui.action.ItemIconAct +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.data.ItemIconId +import com.ivy.data.category.Category +import com.ivy.data.category.CategoryState +import com.ivy.data.category.CategoryType +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l0_system.color.toComposeColor +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@SuppressLint("StaticFieldLeak") +@HiltViewModel +internal class EditCategoryViewModel @Inject constructor( + @ApplicationContext + private val appContext: Context, + private val itemIconAct: ItemIconAct, + private val writeCategoriesAct: WriteCategoriesAct, + private val categoryById: CategoryByIdAct, + private val categoriesFlow: CategoriesFlow, + private val mapCategoryUiAct: MapCategoryUiAct, +) : SimpleFlowViewModel() { + override val initialUi = EditCategoryState( + categoryId = "", + icon = ItemIcon.Sized( + iconM = R.drawable.ic_custom_category_m, + iconS = R.drawable.ic_custom_category_s, + iconL = R.drawable.ic_custom_category_l, + iconId = null + ), + color = Purple, + initialName = "", + parent = null, + archived = false, + type = CategoryType.Both, + ) + + private val category = MutableStateFlow(null) + private var name = "" + private val initialName = MutableStateFlow(initialUi.initialName) + private val iconId = MutableStateFlow(null) + private val color = MutableStateFlow(initialUi.color) + private val parentCategoryId = MutableStateFlow(null) + private val archived = MutableStateFlow(initialUi.archived) + private val type = MutableStateFlow(initialUi.type) + + override val uiFlow: Flow = combine( + category, headerFlow(), secondaryFlow(), parentFlow() + ) { category, header, secondary, parent -> + EditCategoryState( + categoryId = category?.id?.toString() ?: "", + icon = itemIconAct(ItemIconAct.Input(header.iconId, DefaultTo.Category)), + initialName = header.initialName, + color = header.color, + parent = parent, + archived = secondary.archived, + type = secondary.type, + ) + } + + private fun headerFlow(): Flow
= combine( + iconId, initialName, color, + ) { iconId, initialName, color -> + Header(iconId = iconId, initialName = initialName, color = color) + } + + private fun secondaryFlow(): Flow = combine( + type, archived + ) { type, archived -> + Secondary(type, archived) + } + + private fun parentFlow(): Flow = combine( + categoriesFlow(), parentCategoryId + ) { categories, parentId -> + categories.firstOrNull { it.id.toString() == parentId } + ?.let { mapCategoryUiAct(it) } + } + + + // region Event Handling + override suspend fun handleEvent(event: EditCategoryEvent) = when (event) { + is EditCategoryEvent.Initial -> handleInitial(event) + EditCategoryEvent.EditCategory -> editCategory() + is EditCategoryEvent.IconChange -> handleIconPick(event) + is EditCategoryEvent.NameChange -> handleNameChange(event) + is EditCategoryEvent.ColorChange -> handleColorChange(event) + is EditCategoryEvent.ParentChange -> handleFolderChange(event) + is EditCategoryEvent.TypeChange -> handleTypeChange(event) + EditCategoryEvent.Archive -> handleArchive() + EditCategoryEvent.Unarchive -> handleUnarchive() + EditCategoryEvent.Delete -> handleDelete() + } + + private suspend fun handleInitial(event: EditCategoryEvent.Initial) { + // we need a snapshot of the category at this given point in time + // => flow isn't good for that use-case + categoryById(event.categoryId)?.let { + category.value = it + name = it.name + initialName.value = it.name + iconId.value = it.icon + color.value = it.color.toComposeColor() + parentCategoryId.value = it.parentCategoryId?.toString() + type.value = it.type + archived.value = it.state == CategoryState.Archived + } + } + + private suspend fun editCategory() { + val updated = category.value?.copy( + name = name, + color = color.value.toArgb(), + parentCategoryId = parentCategoryId.value?.toUUID(), + icon = iconId.value, + type = type.value, + ) + if (updated != null) { + writeCategoriesAct(Modify.save(updated)) + } + } + + private fun handleIconPick(event: EditCategoryEvent.IconChange) { + iconId.value = event.iconId + } + + private fun handleNameChange(event: EditCategoryEvent.NameChange) { + name = event.name + } + + private fun handleColorChange(event: EditCategoryEvent.ColorChange) { + color.value = event.color + } + + private fun handleFolderChange(event: EditCategoryEvent.ParentChange) { + parentCategoryId.value = event.parent?.id + } + + private fun handleTypeChange(event: EditCategoryEvent.TypeChange) { + type.value = event.type + } + + private suspend fun handleArchive() { + archived.value = true + updateArchived(state = CategoryState.Archived) + showToast("Category archived") + } + + private suspend fun handleUnarchive() { + archived.value = false + updateArchived(state = CategoryState.Default) + showToast("Category unarchived") + } + + private fun showToast(text: String) { + Toast.makeText(appContext, text, Toast.LENGTH_LONG).show() + } + + private suspend fun updateArchived(state: CategoryState) { + val updated = category.value?.copy(state = state) + if (updated != null) { + writeCategoriesAct(Modify.save(updated)) + } + } + + private suspend fun handleDelete() { + category.value?.let { + writeCategoriesAct(Modify.delete(it.id.toString())) + } + } + // endregion + + private data class Header( + val iconId: ItemIconId?, + val initialName: String, + val color: Color, + ) + + private data class Secondary( + val type: CategoryType, + val archived: Boolean, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/edit/component/DeleteCategoryModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/edit/component/DeleteCategoryModal.kt new file mode 100644 index 0000000000..54f38d7c84 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/edit/component/DeleteCategoryModal.kt @@ -0,0 +1,125 @@ +package com.ivy.core.ui.category.edit.component + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Body +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.DeleteCategoryModal( + modal: IvyModal, + level: Int = 1, + archived: Boolean, + categoryName: String, + onArchive: () -> Unit, + onDelete: () -> Unit, +) { + Modal( + modal = modal, + level = level, + actions = { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Focused, + feeling = Feeling.Negative, + text = "Delete forever", + icon = R.drawable.ic_round_delete_forever_24 + ) { + modal.hide() + onDelete() + } + } + ) { + Title( + text = "Delete \"$categoryName\" category forever?", + color = UI.colors.red + ) + SpacerVer(height = 24.dp) + Body( + text = bodyText( + categoryname = categoryName, + archived = archived + ) + ) + if (!archived) { + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "Archive", + icon = R.drawable.round_archive_24 + ) { + modal.hide() + onArchive() + } + } + SpacerVer(height = 48.dp) + } +} + +private fun bodyText( + categoryname: String, + archived: Boolean +): String { + val baseText = + "DANGER! Deleting \"$categoryname\" category will make all of its transactions " + + "\"unspecified\" (uncategorized). This operation CANNOT be undone and " + + "will affect your statistics!" + + " Please, be careful otherwise you may lose your data." + + val unarchivedText = + "\n\nIf you don't want to see this category but want preserve its transactions," + + " a better option would be to just archive it." + return if (archived) baseText else baseText + unarchivedText +} + +// region Preview +@Preview +@Composable +private fun Preview_Unarchived() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + DeleteCategoryModal( + modal = modal, + categoryName = "Category 1", + archived = false, + onArchive = {}, + onDelete = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Archived() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + DeleteCategoryModal( + modal = modal, + categoryName = "Category 1", + archived = true, + onArchive = {}, + onDelete = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerEvent.kt new file mode 100644 index 0000000000..2b8088ba56 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerEvent.kt @@ -0,0 +1,13 @@ +package com.ivy.core.ui.category.pick + +import com.ivy.core.ui.category.pick.data.SelectableCategoryUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.data.transaction.TransactionType + +sealed interface CategoryPickerEvent { + data class Initial(val trnType: TransactionType?) : CategoryPickerEvent + data class CategorySelected(val category: CategoryUi?) : CategoryPickerEvent + + data class ExpandParent(val parent: SelectableCategoryUi) : CategoryPickerEvent + object CollapseParent : CategoryPickerEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerModal.kt new file mode 100644 index 0000000000..7c7cdac0ea --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerModal.kt @@ -0,0 +1,227 @@ +package com.ivy.core.ui.category.pick + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.category.create.CreateCategoryModal +import com.ivy.core.ui.category.pick.component.PickerCategoriesRow +import com.ivy.core.ui.category.pick.component.PickerParentCategory +import com.ivy.core.ui.category.pick.data.CategoryPickerItemUi +import com.ivy.core.ui.category.pick.data.SelectableCategoryUi +import com.ivy.core.ui.category.pick.data.dummySelectableCategoryUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.data.transaction.TransactionType +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe + +@Composable +fun BoxScope.CategoryPickerModal( + modal: IvyModal, + level: Int = 1, + trnType: TransactionType?, + selected: CategoryUi?, + onPick: (CategoryUi?) -> Unit, +) { + val viewModel: CategoryPickerViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel = viewModel, preview = ::previewState) + + LaunchedEffect(trnType) { + viewModel?.onEvent(CategoryPickerEvent.Initial(trnType)) + } + + LaunchedEffect(selected) { + viewModel?.onEvent(CategoryPickerEvent.CategorySelected(selected)) + viewModel?.onEvent(CategoryPickerEvent.CollapseParent) + } + + val createCategoryModal = rememberIvyModal() + + Modal( + modal = modal, + level = level, + actions = { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Neutral, + text = "Unspecified", + icon = R.drawable.ic_custom_category_s, + ) { + viewModel?.onEvent(CategoryPickerEvent.CategorySelected(null)) + onPick(null) + modal.hide() + } + } + ) { + LazyColumn { + item(key = "modal_title") { + Title(text = stringResource(id = R.string.choose_category)) + SpacerVer(height = 16.dp) + } + pickerItems( + items = state.items, + onCategorySelect = { + viewModel?.onEvent(CategoryPickerEvent.CategorySelected(it)) + onPick(it) + modal.hide() + }, + onExpandParent = { + viewModel?.onEvent(CategoryPickerEvent.ExpandParent(it)) + }, + ) + item(key = "add_category_btn") { + AddCategoryButton { + createCategoryModal.show() + } + } + item(key = "last_item_space") { + SpacerVer(height = 24.dp) + } + } + } + + CreateCategoryModal( + modal = createCategoryModal, + level = level + 1, + ) +} + +private fun LazyListScope.pickerItems( + items: List, + onCategorySelect: (CategoryUi) -> Unit, + onExpandParent: (SelectableCategoryUi) -> Unit, +) { + items( + items = items, + key = { + when (it) { + is CategoryPickerItemUi.CategoriesRow -> it.categories.first().category.id + is CategoryPickerItemUi.ParentCategory -> it.parent.category.id + } + } + ) { item -> + when (item) { + is CategoryPickerItemUi.CategoriesRow -> { + SpacerVer(height = 12.dp) + PickerCategoriesRow( + categories = item.categories, + onSelect = { onCategorySelect(it.category) } + ) + } + is CategoryPickerItemUi.ParentCategory -> { + SpacerVer(height = 12.dp) + PickerParentCategory( + item = item, + onParentClick = { + if (item.expanded) { + onCategorySelect(item.parent.category) + } else { + onExpandParent(item.parent) + } + }, + onChildClick = { onCategorySelect(it) } + ) + } + } + } +} + +@Composable +private fun AddCategoryButton( + onClick: () -> Unit, +) { + IvyButton( + modifier = Modifier + .padding(top = 12.dp) + .padding(start = 12.dp), + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = stringResource(R.string.add_category), + icon = R.drawable.ic_round_add_24, + onClick = onClick, + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + CategoryPickerModal( + modal = modal, + trnType = TransactionType.Expense, + selected = dummyCategoryUi(), + onPick = {} + ) + } +} + +private fun previewState() = CategoryPickerState( + items = listOf( + CategoryPickerItemUi.CategoriesRow( + categories = listOf( + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + ) + ), + CategoryPickerItemUi.ParentCategory( + parent = dummySelectableCategoryUi(), + expanded = true, + children = listOf( + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + ) + ), + CategoryPickerItemUi.ParentCategory( + parent = dummySelectableCategoryUi(), + expanded = false, + children = listOf( + dummySelectableCategoryUi(), + ) + ), + CategoryPickerItemUi.CategoriesRow( + categories = listOf( + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + ) + ), + CategoryPickerItemUi.ParentCategory( + parent = dummySelectableCategoryUi(), + expanded = true, + children = listOf( + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + ) + ), + ) +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerState.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerState.kt new file mode 100644 index 0000000000..d8356bd43b --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.category.pick + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.category.pick.data.CategoryPickerItemUi + +@Immutable +data class CategoryPickerState( + val items: List +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerViewModel.kt new file mode 100644 index 0000000000..b071c10850 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/CategoryPickerViewModel.kt @@ -0,0 +1,66 @@ +package com.ivy.core.ui.category.pick + +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.pure.util.flattenLatest +import com.ivy.core.ui.category.pick.action.CategoryPickerItemsFlow +import com.ivy.core.ui.data.CategoryUi +import com.ivy.data.transaction.TransactionType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class CategoryPickerViewModel @Inject constructor( + private val categoryPickerItemsFlow: CategoryPickerItemsFlow +) : SimpleFlowViewModel() { + override val initialUi = CategoryPickerState( + items = emptyList() + ) + + private val trnType = MutableStateFlow(null) + private val expandedParent = MutableStateFlow(null) + private val selectedCategory = MutableStateFlow(null) + + override val uiFlow: Flow = combine( + selectedCategory, expandedParent, trnType + ) { selectedCategory, expandedParent, trnType -> + categoryPickerItemsFlow( + CategoryPickerItemsFlow.Input( + selectedCategory = selectedCategory, + expandedParent = expandedParent, + trnType = trnType, + ) + ).map { + CategoryPickerState(items = it) + } + }.flattenLatest() + + + // region Event Handling + override suspend fun handleEvent(event: CategoryPickerEvent) = when (event) { + is CategoryPickerEvent.Initial -> handleInitial(event) + is CategoryPickerEvent.CategorySelected -> handleCategorySelected(event) + is CategoryPickerEvent.ExpandParent -> handleExpandParent(event) + CategoryPickerEvent.CollapseParent -> handleCollapseParent() + } + + private fun handleInitial(event: CategoryPickerEvent.Initial) { + trnType.value = event.trnType + } + + private fun handleCategorySelected(event: CategoryPickerEvent.CategorySelected) { + selectedCategory.value = event.category + } + + private fun handleExpandParent(event: CategoryPickerEvent.ExpandParent) { + expandedParent.value = event.parent.category + } + + private fun handleCollapseParent() { + expandedParent.value = null + } + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/action/CategoryPickerItemsFlow.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/action/CategoryPickerItemsFlow.kt new file mode 100644 index 0000000000..5dce332aee --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/action/CategoryPickerItemsFlow.kt @@ -0,0 +1,81 @@ +package com.ivy.core.ui.category.pick.action + +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.category.CategoriesListFlow +import com.ivy.core.domain.action.data.CategoryListItem +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.category.pick.data.CategoryPickerItemUi +import com.ivy.core.ui.category.pick.data.SelectableCategoryUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.data.transaction.TransactionType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + + +class CategoryPickerItemsFlow @Inject constructor( + private val categoriesListFlow: CategoriesListFlow, + private val mapCategoryUiAct: MapCategoryUiAct, +) : FlowAction>() { + data class Input( + val selectedCategory: CategoryUi?, + val expandedParent: CategoryUi?, + val trnType: TransactionType?, + ) + + override fun Input.createFlow(): Flow> = + categoriesListFlow(CategoriesListFlow.Input(trnType = trnType)) + .map { items -> + items.mapNotNull { item -> + when (item) { + is CategoryListItem.Archived -> null + is CategoryListItem.CategoryHolder -> SelectableCategoryUi( + category = mapCategoryUiAct(item.category), + selected = item.category.id.toString() == selectedCategory?.id, + ) + is CategoryListItem.ParentCategory -> { + val hasSelectedChild = item.children.any { + it.id.toString() == selectedCategory?.id + } + CategoryPickerItemUi.ParentCategory( + parent = SelectableCategoryUi( + category = mapCategoryUiAct(item.parent), + selected = item.parent.id.toString() == selectedCategory?.id || + hasSelectedChild, + ), + expanded = expandedParent?.id == item.parent.id.toString() || + hasSelectedChild, + children = item.children.map { + SelectableCategoryUi( + category = mapCategoryUiAct(it), + selected = it.id.toString() == selectedCategory?.id, + ) + } + ) + } + } + } + }.map { data -> + val res = mutableListOf() + var catsRowAccumulator = mutableListOf() + + data.forEach { + when (it) { + is CategoryPickerItemUi.ParentCategory -> { + if (catsRowAccumulator.isNotEmpty()) { + res.add(CategoryPickerItemUi.CategoriesRow(catsRowAccumulator)) + catsRowAccumulator = mutableListOf() + } + res.add(it) + } + is SelectableCategoryUi -> catsRowAccumulator.add(it) + } + } + + if (catsRowAccumulator.isNotEmpty()) { + res.add(CategoryPickerItemUi.CategoriesRow(catsRowAccumulator)) + } + + res + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/component/PickerCategoriesRow.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/component/PickerCategoriesRow.kt new file mode 100644 index 0000000000..0fa7fe30cb --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/component/PickerCategoriesRow.kt @@ -0,0 +1,102 @@ +package com.ivy.core.ui.category.pick.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.category.pick.data.SelectableCategoryUi +import com.ivy.core.ui.category.pick.data.dummySelectableCategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.data.icon.dummyIconUnknown +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l3_ivyComponents.WrapContentRow +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.thenWhen + +@Composable +internal fun PickerCategoriesRow( + categories: List, + modifier: Modifier = Modifier, + onSelect: (SelectableCategoryUi) -> Unit, +) { + WrapContentRow( + modifier = modifier.padding(horizontal = 8.dp), + items = categories, + itemKey = { it.category.id }, + horizontalMarginBetweenItems = 8.dp, + verticalMarginBetweenRows = 8.dp, + ) { item -> + CategoryItem( + item = item, + onClick = { onSelect(item) }, + ) + } +} + +@Composable +private fun CategoryItem( + item: SelectableCategoryUi, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + val category = item.category + Row( + modifier = modifier + .clip(UI.shapes.rounded) + .thenWhen { + when (item.selected) { + true -> background(category.color, UI.shapes.rounded) + false -> border(1.dp, category.color, UI.shapes.rounded) + } + } + .clickable(onClick = onClick) + .padding(start = 8.dp, end = 16.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val contrast = if (item.selected) + rememberContrast(category.color) else UI.colorsInverted.pure + ItemIcon( + itemIcon = category.icon, + size = IconSize.S, + tint = contrast + ) + SpacerHor(width = 4.dp) + B2(text = category.name, color = contrast) + } +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + PickerCategoriesRow( + categories = listOf( + dummySelectableCategoryUi( + category = dummyCategoryUi( + name = "Car", + icon = dummyIconUnknown(R.drawable.ic_vue_transport_car) + ) + ), + dummySelectableCategoryUi(selected = true), + dummySelectableCategoryUi(), + dummySelectableCategoryUi() + ), + onSelect = {} + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/component/PickerParentCategory.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/component/PickerParentCategory.kt new file mode 100644 index 0000000000..04516764be --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/component/PickerParentCategory.kt @@ -0,0 +1,109 @@ +package com.ivy.core.ui.category.pick.component + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.category.pick.data.CategoryPickerItemUi +import com.ivy.core.ui.category.pick.data.SelectableCategoryUi +import com.ivy.core.ui.category.pick.data.dummySelectableCategoryUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.DividerHor +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.thenWhen + +@Composable +internal fun PickerParentCategory( + item: CategoryPickerItemUi.ParentCategory, + onParentClick: () -> Unit, + onChildClick: (CategoryUi) -> Unit +) { + ParentCategoryItem(parent = item.parent, onClick = onParentClick) + AnimatedVisibility( + visible = item.expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + PickerCategoriesRow( + modifier = Modifier + .padding(top = 12.dp, bottom = 12.dp), + categories = item.children, + onSelect = { onChildClick(it.category) }, + ) + DividerHor() + } + } +} + +@Composable +private fun ParentCategoryItem( + parent: SelectableCategoryUi, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(UI.shapes.rounded) + .thenWhen { + when (parent.selected) { + true -> background(parent.category.color, UI.shapes.rounded) + false -> border(1.dp, parent.category.color, UI.shapes.rounded) + } + } + .clickable(onClick = onClick) + .padding(start = 8.dp, end = 16.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val contrast = if (parent.selected) + rememberContrast(parent.category.color) else UI.colorsInverted.pure + ItemIcon( + itemIcon = parent.category.icon, + size = IconSize.S, + tint = contrast + ) + SpacerHor(width = 8.dp) + B2(text = parent.category.name, color = contrast) + } +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + Column { + PickerParentCategory( + item = CategoryPickerItemUi.ParentCategory( + parent = dummySelectableCategoryUi(), + children = listOf( + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + dummySelectableCategoryUi(), + ), + expanded = true + ), + onParentClick = {}, + onChildClick = {} + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/data/CategoryPickerItemUi.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/data/CategoryPickerItemUi.kt new file mode 100644 index 0000000000..116c8ed183 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/data/CategoryPickerItemUi.kt @@ -0,0 +1,16 @@ +package com.ivy.core.ui.category.pick.data + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface CategoryPickerItemUi { + data class CategoriesRow( + val categories: List + ) : CategoryPickerItemUi + + data class ParentCategory( + val parent: SelectableCategoryUi, + val expanded: Boolean, + val children: List + ) : CategoryPickerItemUi +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pick/data/SelectableCategoryUi.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pick/data/SelectableCategoryUi.kt new file mode 100644 index 0000000000..a754235cbb --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pick/data/SelectableCategoryUi.kt @@ -0,0 +1,19 @@ +package com.ivy.core.ui.category.pick.data + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi + +@Immutable +data class SelectableCategoryUi( + val category: CategoryUi, + val selected: Boolean +) + +fun dummySelectableCategoryUi( + category: CategoryUi = dummyCategoryUi(), + selected: Boolean = false +) = SelectableCategoryUi( + category = category, + selected = selected +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerModal.kt new file mode 100644 index 0000000000..8feed31e8c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerModal.kt @@ -0,0 +1,165 @@ +package com.ivy.core.ui.category.pickparent + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.* +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Negative +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.design.util.thenWhen + +@Composable +fun BoxScope.ParentCategoryPickerModal( + modal: IvyModal, + selected: CategoryUi?, + level: Int = 1, + onPick: (CategoryUi?) -> Unit, +) { + val viewModel: ParentCategoryPickerViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value + ?: previewState() + + Modal( + modal = modal, + level = level, + actions = { + Negative(text = "Remove parent") { + onPick(null) + modal.hide() + } + } + ) { + LazyColumn( + modifier = Modifier.heightIn(min = 0.dp, max = 620.dp), + ) { + item { + Title(text = "Choose parent") + } + categoryItems( + items = state.categories, + selected = selected, + onSelect = { + onPick(it) + modal.hide() + } + ) + item { + SpacerVer(height = 48.dp) // last item spacer + } + } + } +} + +// region Folders +private fun LazyListScope.categoryItems( + items: List, + selected: CategoryUi?, + onSelect: (CategoryUi) -> Unit +) { + this.items( + items = items, + key = { "category_${it.id}" } + ) { category -> + SpacerVer(height = 12.dp) + CategoryItem( + category = category, + selected = category.id == selected?.id + ) { + onSelect(category) + } + } +} + +@Composable +internal fun CategoryItem( + category: CategoryUi, + selected: Boolean, + onClick: () -> Unit +) { + val dynamicContrast = rememberDynamicContrast(category.color) + val contrastColor = rememberContrast(category.color) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(UI.shapes.squared) + .thenWhen { + when (selected) { + true -> background(category.color, UI.shapes.squared) + .border(2.dp, dynamicContrast, UI.shapes.squared) + false -> border(2.dp, dynamicContrast, UI.shapes.squared) + } + } + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val color = if (selected) contrastColor else UI.colorsInverted.pure + ItemIcon( + itemIcon = category.icon, + size = IconSize.S, + tint = color, + ) + SpacerHor(width = 8.dp) + B2(text = category.name, color = color) + } +} +// endregion + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + ParentCategoryPickerModal( + modal = modal, + selected = dummyCategoryUi(id = "selected"), + onPick = {} + ) + } +} + +private fun previewState() = ParentCategoryPickerState( + categories = listOf( + dummyCategoryUi(id = "selected", name = "Category 1", color = Green), + dummyCategoryUi(name = "Category 2", color = Yellow), + dummyCategoryUi(name = "Category 3", color = Purple), + dummyCategoryUi(name = "Category 4", color = Purple), + dummyCategoryUi(name = "Category 5", color = Purple), + dummyCategoryUi(name = "Category 6", color = Purple), + dummyCategoryUi(name = "Category 7", color = Purple), + dummyCategoryUi(name = "Category 8", color = Purple), + dummyCategoryUi(name = "Category 9", color = Purple), + dummyCategoryUi(name = "Category 10", color = Purple), + dummyCategoryUi(name = "Category 11", color = Purple), + dummyCategoryUi(name = "Category 12", color = Purple), + ) +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerState.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerState.kt new file mode 100644 index 0000000000..8fd6fb82ea --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerState.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.category.pickparent + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.data.CategoryUi + +@Immutable +internal data class ParentCategoryPickerState( + val categories: List +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerViewModel.kt new file mode 100644 index 0000000000..14c455e74f --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/pickparent/ParentCategoryPickerViewModel.kt @@ -0,0 +1,30 @@ +package com.ivy.core.ui.category.pickparent + +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.category.CategoriesFlow +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +internal class ParentCategoryPickerViewModel @Inject constructor( + categoriesFlow: CategoriesFlow, + private val mapCategoryUiAct: MapCategoryUiAct, +) : SimpleFlowViewModel() { + override val initialUi = ParentCategoryPickerState(categories = emptyList()) + + override val uiFlow: Flow = + categoriesFlow().map { categories -> + ParentCategoryPickerState( + categories = categories + .filter { it.parentCategoryId == null } + .map { mapCategoryUiAct(it) } + ) + } + + // region Event Handling + override suspend fun handleEvent(event: Unit) {} + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesEvent.kt new file mode 100644 index 0000000000..ecfc274dc3 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesEvent.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.category.reorder + +import com.ivy.core.ui.data.CategoryUi + +sealed interface ReorderCategoriesEvent { + data class Reorder( + val reordered: List + ) : ReorderCategoriesEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesModal.kt b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesModal.kt new file mode 100644 index 0000000000..f95eb5d926 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesModal.kt @@ -0,0 +1,93 @@ +package com.ivy.core.ui.category.reorder + +import ReorderModal +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.* +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.design.util.thenIf + +@Composable +fun BoxScope.ReorderCategoriesModal( + modal: IvyModal, + level: Int = 1, +) { + val viewModel: ReorderCategoriesViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel, preview = ::previewState) + + ReorderModal( + modal = modal, + level = level, + items = state.items, + onReorder = { + viewModel?.onEvent(ReorderCategoriesEvent.Reorder(it)) + } + ) { _, item -> + CategoryCard(category = item) + } +} + + +@Composable +private fun CategoryCard(category: CategoryUi) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) // margin top + .thenIf(category.hasParent) { + padding(start = 24.dp) + } + .padding(start = 8.dp, end = 16.dp) + .background(category.color, UI.shapes.rounded) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val contrast = rememberContrast(category.color) + ItemIcon(itemIcon = category.icon, size = IconSize.S, tint = contrast) + SpacerHor(width = 4.dp) + B2(text = category.name, color = contrast) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + ReorderCategoriesModal(modal = modal) + } +} + +private fun previewState() = ReorderCategoriesStateUi( + items = listOf( + dummyCategoryUi("Category 1", color = Red), + dummyCategoryUi("Category 2", color = Green), + dummyCategoryUi("Category 3", hasParent = true), + dummyCategoryUi("Category 4", hasParent = true, color = Green3Dark), + dummyCategoryUi("Category 5", color = Blue), + dummyCategoryUi("Category 6", color = Yellow), + ), +) + +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesStateUi.kt b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesStateUi.kt new file mode 100644 index 0000000000..abbaa219cd --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesStateUi.kt @@ -0,0 +1,7 @@ +package com.ivy.core.ui.category.reorder + +import com.ivy.core.ui.data.CategoryUi + +data class ReorderCategoriesStateUi( + val items: List +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesViewModel.kt new file mode 100644 index 0000000000..a8488dd565 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/category/reorder/ReorderCategoriesViewModel.kt @@ -0,0 +1,68 @@ +package com.ivy.core.ui.category.reorder + +import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.action.category.CategoriesFlow +import com.ivy.core.domain.action.category.WriteCategoriesAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.category.reorder.ReorderCategoriesViewModel.InternalState +import com.ivy.data.category.Category +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +internal class ReorderCategoriesViewModel @Inject constructor( + categoriesFlow: CategoriesFlow, + private val mapCategoryUiAct: MapCategoryUiAct, + private val writeCategoriesAct: WriteCategoriesAct +) : FlowViewModel() { + override val initialState = InternalState( + categories = emptyList(), + ) + + override val initialUi = ReorderCategoriesStateUi( + items = emptyList(), + ) + + override val stateFlow: Flow = categoriesFlow().map { categories -> + InternalState( + categories = categories, + ) + } + + override val uiFlow: Flow = stateFlow + .map { internalState -> + ReorderCategoriesStateUi( + items = internalState.categories.map { mapCategoryUiAct(it) }, + ) + } + + + // region Event handling + override suspend fun handleEvent(event: ReorderCategoriesEvent) = when (event) { + is ReorderCategoriesEvent.Reorder -> handleReorder(event) + } + + private suspend fun handleReorder(event: ReorderCategoriesEvent.Reorder) { + val categoriesMap = state.value.categories.associateBy { it.id.toString() } + + val reordered = event.reordered.mapIndexedNotNull { index, item -> + categoriesMap[item.id] + ?.copy(orderNum = index.toDouble()) + } + + val expectedCount = uiState.value.items.size + // verify no lost of data + if (reordered.size == expectedCount) { + writeCategoriesAct(Modify.saveMany(reordered)) + } + } + + // endregion + + data class InternalState( + val categories: List, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/color/ColorPickerButton.kt b/core/ui/src/main/java/com/ivy/core/ui/color/ColorPickerButton.kt index 43bdce9a8e..0fb183787b 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/color/ColorPickerButton.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/color/ColorPickerButton.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.remember 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.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -44,6 +45,7 @@ fun ColorPickerButton( fun ColorButton( color: Color, modifier: Modifier = Modifier, + shape: Shape = UI.shapes.rounded, paddingHorizontal: Dp = 24.dp, paddingVertical: Dp = 24.dp, onClick: () -> Unit, @@ -52,9 +54,9 @@ fun ColorButton( val dynamicContrast = rememberDynamicContrast(color) B1Second( modifier = modifier - .clip(UI.shapes.rounded) - .background(color, UI.shapes.rounded) - .border(width = 2.dp, color = dynamicContrast, UI.shapes.rounded) + .clip(shape) + .background(color, shape) + .border(width = 2.dp, color = dynamicContrast, shape) .clickable(onClick = onClick) .padding(horizontal = paddingHorizontal, vertical = paddingVertical), text = "#$colorHex", diff --git a/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerModal.kt index 64dcee2525..f299fa9571 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerModal.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerModal.kt @@ -33,7 +33,7 @@ import com.ivy.design.l2_components.modal.components.Title import com.ivy.design.l2_components.modal.rememberIvyModal import com.ivy.design.l2_components.modal.scope.ModalActionsScope import com.ivy.design.util.IvyPreview -import com.ivy.design.util.hiltViewmodelPreviewSafe +import com.ivy.design.util.hiltViewModelPreviewSafe import com.ivy.design.util.thenIf private val colorItemSize = 48.dp @@ -41,10 +41,11 @@ private val colorItemSize = 48.dp @Composable fun BoxScope.ColorPickerModal( modal: IvyModal, + level: Int = 1, initialColor: Color?, onColorPicked: (Color) -> Unit, ) { - val viewModel: ColorPickerViewModel? = hiltViewmodelPreviewSafe() + val viewModel: ColorPickerViewModel? = hiltViewModelPreviewSafe() val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() var selectedColor by remember(initialColor) { mutableStateOf(initialColor) } @@ -52,6 +53,7 @@ fun BoxScope.ColorPickerModal( Modal( modal = modal, + level = level, actions = { ModalActions( modal = modal, @@ -76,7 +78,11 @@ fun BoxScope.ColorPickerModal( sections( sections = state.sections, selectedColor = selectedColor, - onColorSelect = { selectedColor = it } + onColorSelect = { + selectedColor = it + onColorPicked(it) + modal.hide() + } ) item(key = "color_picker_last_spacer") { SpacerVer(height = 48.dp) } } @@ -85,13 +91,17 @@ fun BoxScope.ColorPickerModal( HexColorPickerModal( modal = hexColorPickerModal, initialColor = selectedColor, - onColorPicked = { selectedColor = it } + onColorPicked = { + selectedColor = it + it.let(onColorPicked) + modal.hide() + } ) } // region ModalActions @Composable -fun ModalActionsScope.ModalActions( +private fun ModalActionsScope.ModalActions( modal: IvyModal, hexColorPickerModal: IvyModal, selectedColor: Color?, diff --git a/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerViewModel.kt index 8036aeef98..fc95b77140 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerViewModel.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/color/picker/ColorPickerViewModel.kt @@ -1,7 +1,7 @@ package com.ivy.core.ui.color.picker import androidx.compose.ui.graphics.Color -import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.SimpleFlowViewModel import com.ivy.core.domain.pure.ui.groupByRows import com.ivy.core.ui.color.picker.data.ColorSectionUi import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,19 +12,16 @@ import javax.inject.Inject @HiltViewModel internal class ColorPickerViewModel @Inject constructor() : - FlowViewModel() { + SimpleFlowViewModel() { companion object { const val COLORS_PER_ROW = 5 } - override fun initialState(): ColorPickerState = ColorPickerState( + override val initialUi = ColorPickerState( sections = listOf() ) - override fun initialUiState(): ColorPickerState = initialState() - - - override fun stateFlow(): Flow = colorSectionsFlow().map { sections -> + override val uiFlow: Flow = colorSectionsFlow().map { sections -> ColorPickerState( sections = sections, ) @@ -48,9 +45,7 @@ internal class ColorPickerViewModel @Inject constructor() : ) private fun colorRows(colors: List): List> = - groupByRows(colors, iconsPerRow = COLORS_PER_ROW) - - override suspend fun mapToUiState(state: ColorPickerState): ColorPickerState = state + groupByRows(colors, itemsPerRow = COLORS_PER_ROW) // region Event Handling diff --git a/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerModal.kt index 2f9bc2f060..9a32cd4d8f 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerModal.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerModal.kt @@ -33,8 +33,9 @@ import com.ivy.design.l2_components.modal.Modal import com.ivy.design.l2_components.modal.components.Choose import com.ivy.design.l2_components.modal.components.Title import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling import com.ivy.design.util.IvyPreview -import com.ivy.design.util.hiltViewmodelPreviewSafe +import com.ivy.design.util.hiltViewModelPreviewSafe @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -44,7 +45,7 @@ fun BoxScope.HexColorPickerModal( level: Int = 3, onColorPicked: (Color) -> Unit ) { - val viewModel: HexColorPickerViewModel? = hiltViewmodelPreviewSafe() + val viewModel: HexColorPickerViewModel? = hiltViewModelPreviewSafe() val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() if (initialColor != null) { @@ -70,6 +71,7 @@ fun BoxScope.HexColorPickerModal( HexInput( initialHex = state.hex, isError = state.color == null, + feeling = state.color?.let(Feeling::Custom) ?: Feeling.Positive, onHexChange = { viewModel?.onEvent(HexColorPickerEvent.Hex(it)) } @@ -90,6 +92,7 @@ fun BoxScope.HexColorPickerModal( private fun HexInput( initialHex: String, isError: Boolean, + feeling: Feeling, onHexChange: (String) -> Unit, ) { val keyboardController = LocalSoftwareKeyboardController.current @@ -104,6 +107,7 @@ private fun HexInput( .fillMaxWidth() .padding(horizontal = 24.dp), isError = isError, + feeling = feeling, type = InputFieldType.SingleLine, typography = InputFieldTypography.Secondary, initialValue = initialHex, diff --git a/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerViewModel.kt index 4655ebc46b..aba27b5385 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerViewModel.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/color/picker/custom/HexColorPickerViewModel.kt @@ -1,6 +1,6 @@ package com.ivy.core.ui.color.picker.custom -import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.SimpleFlowViewModel import com.ivy.design.l0_system.color.fromHex import com.ivy.design.l0_system.color.toHex import dagger.hilt.android.lifecycle.HiltViewModel @@ -11,24 +11,21 @@ import javax.inject.Inject @HiltViewModel internal class HexColorPickerViewModel @Inject constructor( -) : FlowViewModel() { - override fun initialState() = HexColorPickerState( +) : SimpleFlowViewModel() { + override val initialUi = HexColorPickerState( hex = "", color = null, ) - override fun initialUiState(): HexColorPickerState = initialState() - private val hexFlow = MutableStateFlow("") - override fun stateFlow(): Flow = hexFlow.map { hex -> + override val uiFlow: Flow = hexFlow.map { hex -> HexColorPickerState( hex = "#$hex".uppercase(), color = fromHex(hex) ) } - override suspend fun mapToUiState(state: HexColorPickerState) = state // region Event Handling override suspend fun handleEvent(event: HexColorPickerEvent) = when (event) { diff --git a/core/ui/src/main/java/com/ivy/core/ui/component/Badge.kt b/core/ui/src/main/java/com/ivy/core/ui/component/Badge.kt index 537e3728e4..8f046d151c 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/component/Badge.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/component/Badge.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -18,21 +19,23 @@ import com.ivy.core.ui.data.icon.ItemIcon import com.ivy.core.ui.icon.ItemIcon import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.color.Blue2Dark -import com.ivy.design.l0_system.color.rememberContrastColor +import com.ivy.design.l0_system.color.rememberContrast import com.ivy.design.l1_buildingBlocks.Caption -import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.design.util.ComponentPreview import com.ivy.design.util.thenIf +// TODO: Consider unifying and merging with AccountButton + @Composable fun BadgeComponent( text: String, icon: ItemIcon, background: Color, + modifier: Modifier = Modifier, onClick: (() -> Unit)? = null ) { Row( - modifier = Modifier + modifier = modifier .background(background, UI.shapes.fullyRounded) .thenIf(onClick != null) { clip(UI.shapes.fullyRounded) @@ -42,14 +45,16 @@ fun BadgeComponent( .padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically ) { - val contrastColor = rememberContrastColor(background) + val contrastColor = rememberContrast(background) ItemIcon( itemIcon = icon, size = IconSize.S, tint = contrastColor, ) - SpacerHor(width = 4.dp) Caption( + modifier = Modifier + .padding(start = 4.dp) + .widthIn(min = 0.dp, max = 120.dp), text = text, color = contrastColor, fontWeight = FontWeight.ExtraBold diff --git a/core/ui/src/main/java/com/ivy/core/ui/component/ItemIconNameRow.kt b/core/ui/src/main/java/com/ivy/core/ui/component/ItemIconNameRow.kt new file mode 100644 index 0000000000..da511707b0 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/component/ItemIconNameRow.kt @@ -0,0 +1,82 @@ +package com.ivy.core.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.data.icon.dummyIconSized +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberDynamicContrast +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.util.ComponentPreview + +@Composable +fun ItemIconNameRow( + icon: ItemIcon, + color: Color, + initialName: String, + nameInputHint: String, + autoFocusInput: Boolean, + modifier: Modifier = Modifier, + onPickIcon: () -> Unit, + onNameChange: (String) -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ItemIcon( + modifier = Modifier + .clip(UI.shapes.circle) + .background(color, UI.shapes.circle) + .clickable(onClick = onPickIcon) + .padding(all = 4.dp), + itemIcon = icon, + size = IconSize.M, + tint = rememberDynamicContrast(color) + ) + SpacerHor(width = 8.dp) + ItemNameInput( + modifier = Modifier.weight(1f), + initialName = initialName, + hint = nameInputHint, + feeling = Feeling.Custom(color), + autoFocus = autoFocusInput, + onNameChange = onNameChange + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + ItemIconNameRow( + icon = dummyIconSized(R.drawable.ic_custom_account_m), + color = UI.colors.primary, + initialName = "", + nameInputHint = "New account", + autoFocusInput = false, + onPickIcon = {}, + onNameChange = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/component/ItemNameInput.kt b/core/ui/src/main/java/com/ivy/core/ui/component/ItemNameInput.kt new file mode 100644 index 0000000000..dac14cc5f8 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/component/ItemNameInput.kt @@ -0,0 +1,81 @@ +package com.ivy.core.ui.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.core.ui.R +import com.ivy.design.l0_system.UI +import com.ivy.design.l2_components.input.InputFieldType +import com.ivy.design.l2_components.input.IvyInputField +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.util.ComponentPreview + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ItemNameInput( + initialName: String, + modifier: Modifier = Modifier, + feeling: Feeling, + hint: String, + autoFocus: Boolean, + onNameChange: (String) -> Unit, +) { + val focus = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(autoFocus) { + if (autoFocus) { + focus.requestFocus() + keyboardController?.show() + } + } + + IvyInputField( + modifier = modifier + .focusRequester(focus), + type = InputFieldType.SingleLine, + initialValue = initialName, + shape = UI.shapes.fullyRounded, + feeling = feeling, + placeholder = hint, + onValueChange = onNameChange + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview_Empty() { + ComponentPreview { + ItemNameInput( + initialName = "", + hint = stringResource(R.string.account_name), + feeling = Feeling.Positive, + autoFocus = false, + onNameChange = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Filled() { + ComponentPreview { + ItemNameInput( + initialName = "Cash", + hint = stringResource(R.string.account_name), + feeling = Feeling.Positive, + autoFocus = false, + onNameChange = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/component/ScreenBottomBar.kt b/core/ui/src/main/java/com/ivy/core/ui/component/ScreenBottomBar.kt new file mode 100644 index 0000000000..4b0546cb57 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/component/ScreenBottomBar.kt @@ -0,0 +1,86 @@ +package com.ivy.core.ui.component + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.ivy.core.domain.HandlerViewModel +import com.ivy.design.l1_buildingBlocks.SpacerWeight +import com.ivy.design.l3_ivyComponents.BackButton +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.navigation.Navigator +import com.ivy.resources.R +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@Composable +fun BoxScope.ScreenBottomBar( + modifier: Modifier = Modifier, + actions: @Composable () -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .systemBarsPadding() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp) + .zIndex(500f), + verticalAlignment = Alignment.CenterVertically + ) { + val viewModel: BottomBarViewModel? = hiltViewModelPreviewSafe() + BackButton( + modifier = Modifier + ) { + viewModel?.onEvent(BottomBarEvent.Back) + } + SpacerWeight(weight = 1f) + actions() + } +} + +private sealed interface BottomBarEvent { + object Back : BottomBarEvent +} + +@HiltViewModel +private class BottomBarViewModel @Inject constructor( + private val navigator: Navigator, +) : HandlerViewModel() { + override suspend fun handleEvent(event: BottomBarEvent) = when (event) { + BottomBarEvent.Back -> handleBack() + } + + private fun handleBack() { + navigator.back() + } +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + ScreenBottomBar { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.High, + feeling = Feeling.Positive, + text = "New category", + icon = R.drawable.ic_round_add_24 + ) { + + } + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/component/SelectableItem.kt b/core/ui/src/main/java/com/ivy/core/ui/component/SelectableItem.kt new file mode 100644 index 0000000000..16aefd6efb --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/component/SelectableItem.kt @@ -0,0 +1,193 @@ +package com.ivy.core.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.R +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.data.icon.dummyIconUnknown +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.Purple +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l0_system.color.rememberDynamicContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.Caption +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.thenIf +import com.ivy.design.util.thenWhen + +@Composable +fun SelectableItem( + name: String, + icon: ItemIcon, + color: Color, + selected: Boolean, + deselectButton: Boolean, + modifier: Modifier = Modifier, + onSelect: () -> Unit, + onDeselect: () -> Unit, +) { + val dynamicContrast = rememberDynamicContrast(color) + Row( + modifier = modifier + .clip(UI.shapes.fullyRounded) + .thenWhen { + when (selected) { + true -> background(color, UI.shapes.fullyRounded) + .border(2.dp, dynamicContrast, UI.shapes.fullyRounded) + false -> border(1.dp, color, UI.shapes.fullyRounded) + } + } + .clickable(onClick = onSelect) + .thenIf(selected && !deselectButton) { + padding(vertical = 8.dp) + .padding(end = 24.dp) + }, + verticalAlignment = Alignment.CenterVertically + ) { + when (selected) { + true -> SelectedContent( + name = name, + icon = icon, + color = color, + deselectButton = deselectButton, + onDeselect = onDeselect + ) + false -> Content( + name = name, + icon = icon, + color = color + ) + } + } +} + +@Suppress("unused") +@Composable +private fun RowScope.SelectedContent( + name: String, + icon: ItemIcon, + color: Color, + deselectButton: Boolean, + onDeselect: () -> Unit +) { + SpacerHor(width = 12.dp) + val contrastColor = rememberContrast(color) + ItemIcon( + itemIcon = icon, + size = IconSize.S, + tint = contrastColor, + ) + SpacerHor(width = 8.dp) + B2( + text = name, + color = contrastColor, + fontWeight = FontWeight.ExtraBold + ) + if (deselectButton) { + SpacerHor(width = 12.dp) + IvyButton( + modifier = Modifier.padding(all = 4.dp), + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Negative, + text = null, + icon = R.drawable.round_remove_24, + onClick = onDeselect + ) + } +} + +@Suppress("unused") +@Composable +private fun RowScope.Content( + name: String, + color: Color, + icon: ItemIcon, +) { + ItemIcon( + modifier = Modifier + .padding(vertical = 8.dp) + .padding(start = 8.dp), + itemIcon = icon, + size = IconSize.S, + tint = UI.colorsInverted.pure, + ) + Caption( + modifier = Modifier.padding( + start = 8.dp, end = 16.dp + ), + text = name, + color = UI.colorsInverted.pure, + fontWeight = FontWeight.ExtraBold + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview_Selected() { + ComponentPreview { + SelectableItem( + name = "Account", + icon = dummyIconUnknown(R.drawable.ic_vue_building_bank), + color = Purple, + selected = true, + deselectButton = true, + onSelect = {}, + onDeselect = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Selected_noDeselectButton() { + ComponentPreview { + SelectableItem( + name = "Account", + icon = dummyIconUnknown(R.drawable.ic_vue_building_bank), + color = Purple, + selected = true, + deselectButton = false, + onSelect = {}, + onDeselect = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Deselected() { + ComponentPreview { + SelectableItem( + name = "Account", + icon = dummyIconUnknown(R.drawable.ic_vue_building_bank), + color = Purple, + selected = false, + deselectButton = true, + onSelect = {}, + onDeselect = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyModalEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyModalEvent.kt new file mode 100644 index 0000000000..11441dced2 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyModalEvent.kt @@ -0,0 +1,11 @@ +package com.ivy.core.ui.currency + +import com.ivy.core.ui.currency.data.CurrencyUi +import com.ivy.data.CurrencyCode + +internal sealed interface CurrencyModalEvent { + data class Search(val query: String) : CurrencyModalEvent + data class SelectCurrency(val currencyUi: CurrencyUi) : CurrencyModalEvent + data class SelectCurrencyCode(val currencyCode: CurrencyCode) : CurrencyModalEvent + data class Initial(val initialCurrency: CurrencyCode) : CurrencyModalEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyModalState.kt b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyModalState.kt new file mode 100644 index 0000000000..146cca616c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyModalState.kt @@ -0,0 +1,14 @@ +package com.ivy.core.ui.currency + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.currency.data.CurrencyListItem +import com.ivy.core.ui.currency.data.CurrencyUi +import com.ivy.data.CurrencyCode + +@Immutable +internal data class CurrencyModalState( + val items: List, + val suggested: List, + val selectedCurrency: CurrencyUi?, + val searchQuery: String +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyPickerModal.kt new file mode 100644 index 0000000000..a3c703fd6c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyPickerModal.kt @@ -0,0 +1,358 @@ +package com.ivy.core.ui.currency + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.currency.data.CurrencyListItem +import com.ivy.core.ui.currency.data.CurrencyUi +import com.ivy.data.CurrencyCode +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.* +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Choose +import com.ivy.design.l2_components.modal.components.Search +import com.ivy.design.l2_components.modal.components.SearchButton +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.WrapContentRow +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import com.ivy.resources.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun BoxScope.CurrencyPickerModal( + modal: IvyModal, + level: Int = 2, + initialCurrency: CurrencyCode?, + onCurrencyPick: (CurrencyCode) -> Unit, +) { + val viewModel: CurrencyPickerModalViewModel? = hiltViewModelPreviewSafe() + val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() + + LaunchedEffect(initialCurrency) { + if (initialCurrency != null) { + viewModel?.onEvent(CurrencyModalEvent.Initial(initialCurrency = initialCurrency)) + } + } + + var searchBarVisible by remember { mutableStateOf(false) } + + val keyboardController = LocalSoftwareKeyboardController.current + val resetSearch = { + keyboardController?.hide() + viewModel?.onEvent(CurrencyModalEvent.Search(query = "")) + searchBarVisible = false + } + + Modal( + modal = modal, + level = level, + actions = { + SearchButton(searchBarVisible = searchBarVisible) { + if (searchBarVisible) resetSearch() else searchBarVisible = true + } + SpacerHor(width = 8.dp) + Choose { + keyboardController?.hide() + resetSearch() + modal.hide() + state.selectedCurrency?.code?.let(onCurrencyPick) + } + } + ) { + Search( + searchBarVisible = searchBarVisible, + initialSearchQuery = state.searchQuery, + searchHint = "Search (e.g. EUR, USD, BTC)", + resetSearch = resetSearch, + onSearch = { viewModel?.onEvent(CurrencyModalEvent.Search(it)) }, + overlay = { + Suggested( + suggested = state.suggested, + searchBarVisible = searchBarVisible, + selectedCurrency = state.selectedCurrency, + onClick = { + resetSearch() + modal.hide() + viewModel?.onEvent(CurrencyModalEvent.SelectCurrencyCode(it)) + onCurrencyPick(it) + } + ) + } + ) { + item(key = "cp_header") { + Title(text = stringResource(R.string.choose_currency)) + SpacerVer(height = 12.dp) + } + item(key = "cp_selected_currency_${state.selectedCurrency?.code}") { + SelectedCurrency(selectedCurrency = state.selectedCurrency) + } + currencies( + items = state.items, + selectedCurrency = state.selectedCurrency, + onCurrencySelect = { + resetSearch() + modal.hide() + viewModel?.onEvent(CurrencyModalEvent.SelectCurrency(it)) + onCurrencyPick(it.code) + } + ) + item(key = "cp_last_item_spacer") { + // last item spacer + SpacerVer(height = 24.dp) + } + } + } +} + +// region Currencies list +private fun LazyListScope.currencies( + items: List, + selectedCurrency: CurrencyUi?, + onCurrencySelect: (CurrencyUi) -> Unit +) { + items( + items = items, + key = { + when (it) { + is CurrencyListItem.Currency -> "${it.currency.code}${it.currency.name}" + is CurrencyListItem.SectionDivider -> "divider_${it.name}" + } + } + ) { item -> + when (item) { + is CurrencyListItem.Currency -> { + SpacerVer(height = 12.dp) + CurrencyItem( + currency = item.currency, + selected = item.currency == selectedCurrency, + onClick = onCurrencySelect + ) + } + is CurrencyListItem.SectionDivider -> { + SpacerVer(height = 24.dp) + SectionDivider(divider = item) + } + } + } +} + +@Composable +private fun SectionDivider( + divider: CurrencyListItem.SectionDivider +) { + Caption( + modifier = Modifier.padding(start = 32.dp), + text = divider.name, + fontWeight = FontWeight.SemiBold + ) +} + +@Composable +private fun CurrencyItem( + currency: CurrencyUi, + selected: Boolean, + onClick: (CurrencyUi) -> Unit, +) { + val bgColor = if (selected) UI.colors.primary else UI.colors.medium + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(UI.shapes.rounded) + .background(bgColor, UI.shapes.rounded) + .clickable { onClick(currency) } + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val textColor = rememberContrast(bgColor) + B1Second(text = currency.code, fontWeight = FontWeight.ExtraBold, color = textColor) + B2( + modifier = Modifier + .weight(1f) + .padding(start = 24.dp), + text = currency.name, + fontWeight = FontWeight.SemiBold, + color = textColor, + textAlign = TextAlign.End, + overflow = TextOverflow.Ellipsis, + ) + } +} +// endregion + +// region Selected currency +@Composable +private fun SelectedCurrency( + selectedCurrency: CurrencyUi? +) { + if (selectedCurrency != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .background(UI.colors.primary, UI.shapes.squared) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val contrast = rememberContrast(UI.colors.primary) + Column( + modifier = Modifier + .weight(1f) + .padding(end = 16.dp) + ) { + B2( + text = selectedCurrency.code, + color = contrast, + fontWeight = FontWeight.Normal + ) + B1Second( + modifier = Modifier.fillMaxWidth(), + text = selectedCurrency.name, + color = contrast, + fontWeight = FontWeight.ExtraBold, + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis, + ) + } + IconRes(icon = R.drawable.ic_round_check_24, tint = contrast) + SpacerHor(width = 4.dp) + B2( + text = stringResource(R.string.selected), + color = contrast + ) + } + } +} +// endregion + +// region Suggested currencies +@Composable +private fun BoxScope.Suggested( + suggested: List, + searchBarVisible: Boolean, + selectedCurrency: CurrencyUi?, + onClick: (CurrencyCode) -> Unit, +) { + if (suggested.isEmpty()) return + + AnimatedVisibility( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + visible = !searchBarVisible, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(UI.colors.pure, UI.shapes.roundedTop) + .padding(bottom = 4.dp) + .border(1.dp, UI.colors.neutral, UI.shapes.roundedTop) + .padding(top = 12.dp, bottom = 16.dp) + ) { + B1( + modifier = Modifier.padding(start = 24.dp), + text = "Suggested", + ) + SpacerVer(height = 12.dp) + WrapContentRow( + modifier = Modifier + .padding(horizontal = 8.dp), + items = suggested, + itemKey = { "suggested_$it" } + ) { currency -> + SuggestedCurrencyItem( + currencyCode = currency, + selected = currency == selectedCurrency?.code + ) { + onClick(currency) + } + } + } + } +} + +@Composable +private fun SuggestedCurrencyItem( + currencyCode: CurrencyCode, + selected: Boolean, + onClick: () -> Unit +) { + IvyButton( + size = ButtonSize.Small, + visibility = if (selected) Visibility.High else Visibility.Medium, + feeling = Feeling.Positive, + text = currencyCode, + icon = null, + onClick = onClick, + ) +} +// endregion + +// region Previews +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + CurrencyPickerModal( + modal = modal, + initialCurrency = null, + onCurrencyPick = {} + ) + } +} + +private fun previewState() = CurrencyModalState( + items = listOf( + CurrencyListItem.SectionDivider(name = "A"), + CurrencyListItem.Currency(CurrencyUi("BGN", "Bulgarian Lev")), + CurrencyListItem.Currency(CurrencyUi("USD", "US Dollar")), + CurrencyListItem.Currency(CurrencyUi("EUR", "Euro")), + CurrencyListItem.SectionDivider(name = "Crypto"), + CurrencyListItem.Currency(CurrencyUi("BTC", "Bitcoin")), + CurrencyListItem.SectionDivider(name = "Dummy"), + CurrencyListItem.Currency(CurrencyUi("DMY1", "Dummy")), + CurrencyListItem.Currency(CurrencyUi("DMY2", "Dummy")), + CurrencyListItem.Currency(CurrencyUi("DMY3", "Dummy")), + CurrencyListItem.Currency(CurrencyUi("DMY4", "Dummy")), + CurrencyListItem.Currency(CurrencyUi("DMY5", "Dummy")), + ), + suggested = listOf( + "BGN", + "ADA", + "EUR", + "USD", + "GBP", + "INR", + ), + selectedCurrency = CurrencyUi("BGN", "Bulgarian Lev"), + searchQuery = "" +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyPickerModalViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyPickerModalViewModel.kt new file mode 100644 index 0000000000..0c18d3b820 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/currency/CurrencyPickerModalViewModel.kt @@ -0,0 +1,121 @@ +package com.ivy.core.ui.currency + +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountsFlow +import com.ivy.core.ui.currency.data.CurrencyListItem +import com.ivy.core.ui.currency.data.CurrencyUi +import com.ivy.data.CurrencyCode +import com.ivy.data.IvyCurrency +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@HiltViewModel +internal class CurrencyPickerModalViewModel @Inject constructor( + private val accountsFlow: AccountsFlow, +) : SimpleFlowViewModel() { + override val initialUi = CurrencyModalState( + items = emptyList(), + suggested = emptyList(), + selectedCurrency = null, + searchQuery = "", + ) + + private var searchQuery = MutableStateFlow("") + private val selectedCurrency = MutableStateFlow(null) + + override val uiFlow: Flow = combine( + currenciesFlow(), selectedCurrency, suggestedFlow() + ) { currencies, selectedCurrency, suggested -> + CurrencyModalState( + items = currencies, + suggested = suggested, + selectedCurrency = selectedCurrency, + searchQuery = searchQuery.value, + ) + } + + private fun currenciesFlow(): Flow> = combine( + availableCurrenciesFlow(), searchQueryFlow() + ) { allCurrencies, searchQuery -> + val currencies = if (searchQuery != null) + allCurrencies.filter { it.passesSearch(searchQuery) } + else allCurrencies + + currencies.groupBy { it.code.first() } + .toSortedMap() + .flatMap { (letter, currencies) -> + listOf( + CurrencyListItem.SectionDivider(name = letter.uppercase()), + ) + currencies.map { + CurrencyListItem.Currency( + CurrencyUi( + code = it.code, + name = if (it.name.isNotEmpty()) + // capitalize the first letter + "${it.name.first().uppercase()}${it.name.drop(1)}" else "" + ) + ) + } + } + } + + private fun IvyCurrency.passesSearch(searchQuery: String): Boolean = + code.lowercase().contains(searchQuery) || name.lowercase().contains(searchQuery) + + @OptIn(FlowPreview::class) + private fun searchQueryFlow(): Flow = searchQuery.map { + it.lowercase().trim().takeIf(String::isNotEmpty) // normalize search query + }.debounce(100) + + private fun availableCurrenciesFlow(): Flow> = + flowOf(IvyCurrency.getAvailable()) + + private fun suggestedFlow(): Flow> = accountsFlow().map { accounts -> + accounts.map { it.currency }.toSet() + }.map { accountCurrencies -> + accountCurrencies.plus( + listOf( + "USD", + "EUR", + "INR", + "GBP" + ) + ).toList().sorted() + } + + // region Event Handling + override suspend fun handleEvent(event: CurrencyModalEvent) = when (event) { + is CurrencyModalEvent.Search -> handleSearch(event) + is CurrencyModalEvent.SelectCurrency -> handleSelectCurrency(event) + is CurrencyModalEvent.SelectCurrencyCode -> handleSelectCurrencyCode(event) + is CurrencyModalEvent.Initial -> handleInitial(event) + } + + private fun handleSearch(event: CurrencyModalEvent.Search) { + searchQuery.value = event.query + } + + private fun handleSelectCurrency(event: CurrencyModalEvent.SelectCurrency) { + selectedCurrency.value = event.currencyUi + } + + private fun handleSelectCurrencyCode(event: CurrencyModalEvent.SelectCurrencyCode) { + findCurrency(event.currencyCode)?.let { + selectedCurrency.value = it + } + } + + private fun handleInitial(event: CurrencyModalEvent.Initial) { + findCurrency(event.initialCurrency)?.let { + selectedCurrency.value = it + } + } + + private fun findCurrency(code: CurrencyCode): CurrencyUi? = (uiState.value.items + .firstOrNull { + (it as? CurrencyListItem.Currency)?.currency?.code == code + } as? CurrencyListItem.Currency)?.currency + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/currency/data/CurrencyListItem.kt b/core/ui/src/main/java/com/ivy/core/ui/currency/data/CurrencyListItem.kt new file mode 100644 index 0000000000..e8de793b17 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/currency/data/CurrencyListItem.kt @@ -0,0 +1,12 @@ +package com.ivy.core.ui.currency.data + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface CurrencyListItem { + @Immutable + data class Currency(val currency: CurrencyUi) : CurrencyListItem + + @Immutable + data class SectionDivider(val name: String) : CurrencyListItem +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/currency/data/CurrencyUi.kt b/core/ui/src/main/java/com/ivy/core/ui/currency/data/CurrencyUi.kt new file mode 100644 index 0000000000..de74ded725 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/currency/data/CurrencyUi.kt @@ -0,0 +1,10 @@ +package com.ivy.core.ui.currency.data + +import androidx.compose.runtime.Immutable +import com.ivy.data.CurrencyCode + +@Immutable +data class CurrencyUi( + val code: CurrencyCode, + val name: String, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/data/CategoryUi.kt b/core/ui/src/main/java/com/ivy/core/ui/data/CategoryUi.kt index 241050efd2..8175b227c5 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/data/CategoryUi.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/data/CategoryUi.kt @@ -7,6 +7,7 @@ import com.ivy.core.ui.R import com.ivy.core.ui.data.icon.ItemIcon import com.ivy.core.ui.data.icon.dummyIconSized import com.ivy.design.l0_system.color.Purple +import java.util.* @Immutable data class CategoryUi( @@ -14,16 +15,20 @@ data class CategoryUi( val name: String, val color: Color, val icon: ItemIcon, + val hasParent: Boolean, ) fun dummyCategoryUi( name: String = "Category", @ColorInt color: Color = Purple, - icon: ItemIcon = dummyIconSized(R.drawable.ic_custom_category_s) + icon: ItemIcon = dummyIconSized(R.drawable.ic_custom_category_s), + hasParent: Boolean = false, + id: String = UUID.randomUUID().toString(), ) = CategoryUi( - id = "", + id = id, name = name, color = color, icon = icon, + hasParent = hasParent, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/data/AccountUi.kt b/core/ui/src/main/java/com/ivy/core/ui/data/account/AccountUi.kt similarity index 71% rename from core/ui/src/main/java/com/ivy/core/ui/data/AccountUi.kt rename to core/ui/src/main/java/com/ivy/core/ui/data/account/AccountUi.kt index 3a89033078..1e28663466 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/data/AccountUi.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/data/account/AccountUi.kt @@ -1,4 +1,4 @@ -package com.ivy.core.ui.data +package com.ivy.core.ui.data.account import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color @@ -6,6 +6,7 @@ import com.ivy.core.ui.R import com.ivy.core.ui.data.icon.ItemIcon import com.ivy.core.ui.data.icon.dummyIconSized import com.ivy.design.l0_system.color.Green +import java.util.* @Immutable data class AccountUi( @@ -13,15 +14,19 @@ data class AccountUi( val name: String, val color: Color, val icon: ItemIcon, + val excluded: Boolean, ) fun dummyAccountUi( name: String = "Account", + id: String = UUID.randomUUID().toString(), color: Color = Green, - icon: ItemIcon = dummyIconSized(R.drawable.ic_custom_account_s) + icon: ItemIcon = dummyIconSized(R.drawable.ic_custom_account_s), + excluded: Boolean = false, ) = AccountUi( - id = "", + id = id, name = name, color = color, - icon = icon + icon = icon, + excluded = excluded, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/data/account/FolderUi.kt b/core/ui/src/main/java/com/ivy/core/ui/data/account/FolderUi.kt new file mode 100644 index 0000000000..39ff529c63 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/data/account/FolderUi.kt @@ -0,0 +1,32 @@ +package com.ivy.core.ui.data.account + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import com.ivy.core.ui.R +import com.ivy.core.ui.data.icon.ItemIcon +import com.ivy.core.ui.data.icon.dummyIconUnknown +import com.ivy.design.l0_system.color.Purple +import java.util.* + +@Immutable +data class FolderUi( + val id: String, + val name: String, + val icon: ItemIcon, + val color: Color, + val orderNum: Double, +) + +fun dummyFolderUi( + name: String = "Folder", + id: String = UUID.randomUUID().toString(), + icon: ItemIcon = dummyIconUnknown(R.drawable.ic_vue_files_folder), + color: Color = Purple, + orderNum: Double = 0.0, +) = FolderUi( + id = id, + name = name, + icon = icon, + color = color, + orderNum = orderNum, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/DueSectionUi.kt b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/DueSectionUi.kt index 4c7c379ec0..85a94f90be 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/DueSectionUi.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/DueSectionUi.kt @@ -8,7 +8,7 @@ data class DueSectionUi( val dueType: DueSectionUiType, val income: ValueUi?, val expense: ValueUi?, - val trns: List, + val trns: List, ) @Immutable @@ -20,7 +20,7 @@ fun dummyDueSectionUi( dueType: DueSectionUiType, income: ValueUi?, expense: ValueUi?, - trns: List = emptyList() + trns: List = emptyList() ) = DueSectionUi( dueType = dueType, income = income, expense = expense, trns = trns ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TransactionUi.kt b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TransactionUi.kt index 7d704a985f..2d3fe29a79 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TransactionUi.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TransactionUi.kt @@ -3,9 +3,9 @@ package com.ivy.core.ui.data.transaction import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import com.ivy.core.domain.pure.format.ValueUi -import com.ivy.core.ui.data.AccountUi import com.ivy.core.ui.data.CategoryUi -import com.ivy.core.ui.data.dummyAccountUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi import com.ivy.core.ui.data.dummyCategoryUi import com.ivy.data.transaction.TransactionType import java.util.* diff --git a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnListItemUi.kt b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnListItemUi.kt index f1f941d864..4d9099de79 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnListItemUi.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnListItemUi.kt @@ -19,10 +19,12 @@ sealed interface TrnListItemUi { @Immutable data class DateDivider( + val id: String, val date: String, val day: String, val cashflow: ValueUi, val positiveCashflow: Boolean, + val collapsed: Boolean, ) : TrnListItemUi } diff --git a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnTimeUi.kt b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnTimeUi.kt index ea4af02353..17109d0d53 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnTimeUi.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/data/transaction/TrnTimeUi.kt @@ -3,6 +3,8 @@ package com.ivy.core.ui.data.transaction import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.platform.LocalContext +import com.ivy.common.time.deviceTimeProvider +import com.ivy.common.time.format import com.ivy.common.time.timeNow import com.ivy.core.ui.time.formatNicely import java.time.LocalDateTime @@ -10,11 +12,15 @@ import java.time.LocalDateTime @Immutable sealed interface TrnTimeUi { @Immutable - data class Actual(val actual: String) : TrnTimeUi + data class Actual( + val actualDate: String, + val actualTime: String, + ) : TrnTimeUi @Immutable data class Due( - val dueOn: String, + val dueOnDate: String, + val dueOnTime: String, val upcoming: Boolean, ) : TrnTimeUi } @@ -22,10 +28,23 @@ sealed interface TrnTimeUi { @Composable fun dummyTrnTimeActualUi( time: LocalDateTime = timeNow() -) = TrnTimeUi.Actual(time.formatNicely(LocalContext.current).uppercase()) +) = TrnTimeUi.Actual( + actualDate = time.formatNicely( + LocalContext.current, + deviceTimeProvider(), + ).uppercase(), + actualTime = time.format("HH:mm"), +) @Composable fun dummyTrnTimeDueUi( time: LocalDateTime = timeNow().plusHours(1), upcoming: Boolean = true, -) = TrnTimeUi.Due(time.formatNicely(LocalContext.current).uppercase(), upcoming = upcoming) \ No newline at end of file +) = TrnTimeUi.Due( + dueOnDate = time.formatNicely( + LocalContext.current, + deviceTimeProvider() + ).uppercase(), + dueOnTime = time.format("HH:mm"), + upcoming = upcoming +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerIcons.kt b/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerIcons.kt index 259b6af611..40c63cac9e 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerIcons.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerIcons.kt @@ -80,19 +80,19 @@ private fun ivyIcons(): List = listOf( Icon( "star", keywords = listOf( "stars", "favorites", "favourites", "tops", "reviews", - "success", "achievements", "christmas", "xmas" + "success", "achievements", "christmas", "xmas", "premium" ) ), Icon( "education", keywords = listOf( - "education", "school", "university", "study", - "learn", "hats", "academy" + "education", "school", "university", "study", "learning", "hats", "academy", + "high school" ) ), Icon( "fitness", keywords = listOf( - "fitness", "gym", "workout", "train", "weights", - "sport", "lift", "dumbbells", "workout", "work out" + "fitness", "gym", "workouts", "train", "weights", "sports", "lifting", "dumbbells", + "work out" ) ), Icon( @@ -153,7 +153,7 @@ private fun ivyIcons(): List = listOf( ), Icon( "birthday", keywords = listOf( - "births", "cakes", "candles", "surprises", "bdays", + "birthdays", "cakes", "candles", "surprises", "bdays", "b-day" ) ), @@ -165,9 +165,9 @@ private fun ivyIcons(): List = listOf( ), Icon( "camera", keywords = listOf( - "cameras", "videos", "edits", "photos", "movies", "records", "directing", - "tickets", "studios", "shows", "tvs", "streams", "acts", "actions", "produces", - "productions", "acting" + "cameras", "videos", "editing", "photos", "movies", "records", "directing", "studios", + "shows", "tvs", "streams", "acts", "actions", "produces", "productions", "acting", + "films", "vlogs", "vlogging" ) ), Icon( @@ -178,7 +178,7 @@ private fun ivyIcons(): List = listOf( ), Icon( "coffee", keywords = listOf( - "coffees", "cafes", "hot", "mornings", "wake up", + "coffees", "cafes", "hot", "mornings", "wake up", "warm", "energy", "drinks", "fun", "cups", "mugs", "glasses" ) ), @@ -202,12 +202,12 @@ private fun ivyIcons(): List = listOf( ), Icon( "document", keywords = listOf( - "documents", "papers", "lists", "notes", "texts", + "documents", "papers", "lists", "notes", "texts", "agenda", "messages", "news", "magazines", "diary", "plans", "tasks", "organise", "organize", "bills", "taxes", "fees", "accounts", "reports", "receipts", "recipes", "prescripts", "labels", "orders", "warranty", "insurances", "policy", "scripts", "content", "write", "copy", "writing", "create", "assignments", "to-do", "todos", "contracts", "library", - "tests", "exams", "portfolios", "cvs" + "tests", "exams", "portfolios", "cvs", "articles" ) ), Icon( @@ -265,7 +265,7 @@ private fun ivyIcons(): List = listOf( Icon( "game", keywords = listOf( "games", "gaming", "plays", "consoles", "ps", "pc", "nintendos", "xboxes", - "hobby", "spare", "free", "leisure", "chill", "computers" + "hobby", "spare", "free", "leisure", "chill", "computers", "computers" ) ), Icon( @@ -277,7 +277,7 @@ private fun ivyIcons(): List = listOf( Icon( "gift", keywords = listOf( "gifts", "party", "celebrate", "celebrations", "presents", "donations", "donates", - "births", "bdays", "b-day", "holidays" + "birthdays", "bdays", "b-day", "holidays" ) ), Icon( @@ -309,7 +309,7 @@ private fun ivyIcons(): List = listOf( "house", keywords = listOf( "houses", "mortgages", "homes", "apartments", "buildings", "property", "chores", "estates", "accommodations", "rents", "sales", "airbnb", "lives", - "places", "hosts", "living" + "places", "hosts", "living", "remotely" ) ), Icon( @@ -334,7 +334,7 @@ private fun ivyIcons(): List = listOf( Icon( "location", keywords = listOf( "locations", "gps", "places", "maps", "address", - "live", "delivery" + "live", "delivery", "geography" ) ), Icon( @@ -346,9 +346,9 @@ private fun ivyIcons(): List = listOf( ), Icon( "music", keywords = listOf( - "music", "headsets", "headphones", "sounds", "spotify", - "notes", "singers", "songs", "hear", "fun", "party", "records", "directing", "radios", - "produce", "production", "hits" + "music", "headsets", "headphones", "sounds", "spotify", "singers", "songs", "hear", + "fun", "party", "records", "directing", "radios", "produce", "production", "hits", + "tunes", "performing", "recordings" ) ), Icon( @@ -467,19 +467,19 @@ private fun ivyIcons(): List = listOf( ), Icon( "zeus", keywords = listOf( - "zeus", "lightning", "rain", "emergency", "urgents", - "storms", "flash", "thunders", "important", "priority" + "zeus", "lightning", "rains", "emergency", "urgents", "sparks", + "storms", "flash", "thunders", "important", "priority", "thor" ) ), Icon( "calendar", keywords = listOf( "calendars", "plans", "schedules", "memos", - "planners", "notes", "tasks", "priority" + "planners", "notes", "tasks", "priority", "agenda" ) ), Icon( "crown", keywords = listOf( - "crowns", "luxury", "vip", "tops", "queens", "kings", + "crowns", "luxury", "vip", "top", "queens", "kings", "the best", "luxe" ) ), @@ -487,7 +487,7 @@ private fun ivyIcons(): List = listOf( "diamond", keywords = listOf( "diamonds", "luxury", "luxe", "vip", "weddings", "rings", "tops", "expensive", "glamorous", "shine", "shining", "sparkling", "brilliant", - "glory", "sparkle" + "glory", "sparkle", "premium" ) ), Icon( @@ -560,14 +560,16 @@ private fun vueBrands(): List = listOf( Icon( "ic_vue_brands_messenger", keywords = listOf( - "messenger", "messages", "chatting", "chats", "talking", "communication", "communicate" + "messenger", "messages", "chatting", "chats", "talking", "communication", "communicate", + "sending", "texting" ) ), Icon("ic_vue_brands_facebook", keywords = listOf("facebook", "fb", "social media")), Icon("ic_vue_brands_framer", keywords = listOf("framer", "web builder", "websites")), Icon( "ic_vue_brands_whatsapp", keywords = listOf( - "whatsapp", "communication", "communicate", "messages", "chatting", "chats", "talking" + "whatsapp", "communication", "communicate", "messages", "chatting", "chats", "talking", + "sending", "texting" ) ), Icon( @@ -597,7 +599,7 @@ private fun vueBrands(): List = listOf( Icon( "ic_vue_brands_apple", keywords = listOf( "apple", "iphone", "ipad", "macbook", - "iwatch", "laptops", "technology" + "iwatch", "laptops", "technology", "ios" ) ), Icon( @@ -621,7 +623,8 @@ private fun vueBrands(): List = listOf( ), Icon( "ic_vue_brands_photoshop", keywords = listOf( - "photoshop", "ps", "designers", "photos", "technology", "software", "editing" + "photoshop", "ps", "designers", "photos", "technology", "software", "editing", "pics", + "pictures", "images", "photography" ) ), Icon( @@ -646,7 +649,11 @@ private fun vueBrands(): List = listOf( "illustrations", "designers", "creative", "create", "software" ) ), - Icon("ic_vue_brands_xiaomi", keywords = listOf("xiaomi", "phones", "technology")), + Icon( + "ic_vue_brands_xiaomi", keywords = listOf( + "xiaomi", "phones", "technology", "android" + ) + ), Icon("ic_vue_brands_windows", keywords = listOf("windows", "operational system", "os")), Icon( "ic_vue_brands_snapchat", keywords = listOf( @@ -686,12 +693,12 @@ private fun vueBuilding(): List = listOf( ), Icon( "ic_vue_building_house", keywords = listOf( - "buildings", "houses", "homes", "couples", "love", "live" + "buildings", "houses", "homes", "couples", "love", "live", "remotely" ) ), Icon( "ic_vue_building_courthouse", keywords = listOf( - "courthouses", "lawyers", "legal", "businesses", "institutions" + "courthouses", "lawyers", "legal", "businesses", "institutions", "judges" ) ), ) @@ -1062,149 +1069,650 @@ private fun vueDelivery(): List = listOf( "boxes", "cubes", "delivery", "delivering", "orders", "purchases" ) ), - Icon("ic_vue_delivery_truck", keywords = listOf( - "truck", "delivery", "delivering", "packages", "orders", "give", "take", "buy", "sell", - "sales", "packets", "cars", "vehicles", "receiving", "receive", "replacement", "exchange", - "swap", "gifts", "purchases", "dhl", "amazon")), + Icon( + "ic_vue_delivery_truck", keywords = listOf( + "truck", + "delivery", + "delivering", + "packages", + "orders", + "give", + "take", + "buy", + "sell", + "sales", + "packets", + "cars", + "vehicles", + "receiving", + "receive", + "replacement", + "exchange", + "swap", + "gifts", + "purchases", + "dhl", + "amazon" + ) + ), ) // endregion // region Design (Vue) private fun vueDesign(): List = listOf( - Icon("ic_vue_design_bezier"), - Icon("ic_vue_design_brush"), - Icon("ic_vue_design_color_swatch"), - Icon("ic_vue_design_scissors"), - Icon("ic_vue_design_magicpen"), - Icon("ic_vue_design_roller"), - Icon("ic_vue_design_tool_pen"), + Icon( + "ic_vue_design_bezier", keywords = listOf( + "bezier", "curves", "graph", "designers", "css", "technology", "tools", "drawings", + "sketches" + ) + ), + Icon( + "ic_vue_design_brush", keywords = listOf( + "brushes", "designers", "paintings", "pictures", "art", "decorations", "decorate", + "decorating" + ) + ), + Icon( + "ic_vue_design_color_swatch", keywords = listOf( + "swatches", "designers", "fashion", "interiors", "art", "decorations", "decorate", + "decorating" + ) + ), + Icon( + "ic_vue_design_scissors", keywords = listOf( + "scissors", "designers", "cutting", "tools", "diy" + ) + ), + Icon( + "ic_vue_design_magicpen", keywords = listOf( + "magic pen", "pens", "magical", "colorful", "colourful", "fairy", "decorations", + "decorate", "decorating", "notes" + ) + ), + Icon( + "ic_vue_design_roller", keywords = listOf( + "rollers", "painters", "designers", "repairs", "repairments", "decorate", "decorating", + "decorations" + ) + ), + Icon( + "ic_vue_design_tool_pen", keywords = listOf( + "bezier", "curves", "graph", "designers", "css", "technology", "pens", "tools", + "drawings", "paintings", "sketches", "notes" + ) + ), ) // endregion // region Dev (Vue) private fun vueDev(): List = listOf( - Icon("ic_vue_dev_code"), - Icon("ic_vue_dev_hierarchy"), - Icon("ic_vue_dev_relation"), - Icon("ic_vue_dev_arrow"), - Icon("ic_vue_dev_data"), - Icon("ic_vue_dev_hashtag"), + Icon( + "ic_vue_dev_code", keywords = listOf( + "programming", "programmer", "coder", "coding", "software", "logician", "engineers", + "engineering", "it", "technology", "developers", "programs", "development", "developing" + ) + ), + Icon( + "ic_vue_dev_hierarchy", keywords = listOf( + "programming", "programmer", "coder", "coding", "softwares", "logician", "engineers", + "engineering", "it", "technology", "hierarchy", "developers", "programs", "development", + "developing", "structures", "relations" + ) + ), + Icon( + "ic_vue_dev_relation", keywords = listOf( + "programming", "programmer", "coder", "coding", "softwares", "logician", "engineers", + "engineering", "it", "technology", "hierarchy", "developers", "programs", "development", + "developing", "structures", "relations" + ) + ), + Icon( + "ic_vue_dev_arrow", keywords = listOf( + "programming", "programmer", "coder", "coding", "softwares", "logician", "engineers", + "engineering", "it", "technology", "hierarchy", "developers", "programs", "development", + "developing", "structures", "relations", "arrows" + ) + ), + Icon( + "ic_vue_dev_data", keywords = listOf( + "programming", "programmer", "coder", "coding", "softwares", "logician", "engineers", + "engineering", "it", "technology", "hierarchy", "developers", "programs", "development", + "developing", "structures", "relations", "data" + ) + ), + Icon( + "ic_vue_dev_hashtag", keywords = listOf( + "programming", "programmer", "coder", "coding", "softwares", "logician", "engineers", + "engineering", "it", "technology", "hierarchy", "developers", "programs", "development", + "developing", "structures", "relations", "social media", "hashtag" + ) + ), ) // endregion // region Education (Vue) private fun vueEducation(): List = listOf( - Icon("ic_vue_edu_planer"), - Icon("ic_vue_edu_briefcase"), - Icon("ic_vue_edu_award"), - Icon("ic_vue_edu_glass"), - Icon("ic_vue_edu_graduate_cap"), - Icon("ic_vue_edu_calculator"), - Icon("ic_vue_edu_note"), - Icon("ic_vue_edu_magazine"), - Icon("ic_vue_edu_pen"), - Icon("ic_vue_edu_telescope"), - Icon("ic_vue_edu_book"), - Icon("ic_vue_edu_ruler_pen"), - Icon("ic_vue_edu_todo"), - Icon("ic_vue_edu_omega"), - Icon("ic_vue_edu_bookmark"), + Icon( + "ic_vue_edu_planer", keywords = listOf( + "planners", + "logbooks", + "calendars", + "organizers", + "organisers", + "appointments", + "diary", + "notes", + "notebooks", + "schedules", + "agenda" + ) + ), + Icon( + "ic_vue_edu_briefcase", keywords = listOf( + "briefcases", "suitcases", "working", "careers", "professions", "appointments", + "occupations" + ) + ), + Icon( + "ic_vue_edu_award", keywords = listOf( + "awards", "badges", "prizes", "rewards", "premium", "stars" + ) + ), + Icon( + "ic_vue_edu_glass", keywords = listOf( + "glass", "cones", "vases", "flasks", "chemistry", "cones", "sciences", "potions", + "elixirs", "pharmacy", "labs", "education", "study", "learning" + ) + ), + Icon( + "ic_vue_edu_graduate_cap", keywords = listOf( + "education", "graduate", "graduation", "caps", "hats", "students", "graduates", "study", + "learning", "high school", "academy" + ) + ), + Icon( + "ic_vue_edu_calculator", keywords = listOf( + "calculates", "calculators", "calculations", "maths", "numbers", "finances" + ) + ), + Icon( + "ic_vue_edu_note", keywords = listOf( + "notes", "bills", "receipts", "recipes", "reports", "invoices", "fees", "taxes", + "expenses", "flashcards", "education", "study", "learning" + ) + ), + Icon( + "ic_vue_edu_magazine", keywords = listOf( + "magazines", "newspapers", "diary", "planners", "notes", "readings", + "education", "study", "learning", "notebooks", "textbooks", "agenda" + ) + ), + Icon( + "ic_vue_edu_pen", keywords = listOf( + "pens", "drawings", "notes", "designers", "css", "technology", "pens", "paintings", + "sketches", "study", "learning", "notes", "pencils" + ) + ), + Icon( + "ic_vue_edu_telescope", keywords = listOf( + "stars", "telescope", "sky", "planets", "astronomy", "sciences" + ) + ), + Icon( + "ic_vue_edu_book", keywords = listOf( + "notebooks", "textbooks", "planners", "logbooks", "organizers", "organisers", + "appointments", "diary", "notes", "agenda" + ) + ), + Icon( + "ic_vue_edu_ruler_pen", keywords = listOf( + "rulers", "pens", "drawings", "measure", "pencils" + ) + ), + Icon( + "ic_vue_edu_todo", keywords = listOf( + "todos", "to do", "to-do", "tasks", "check marks", "ticks", "schedules", "plans", + "agenda" + ) + ), + Icon( + "ic_vue_edu_omega", keywords = listOf( + "omega", "maths", "symbols", "signs", "letters" + ) + ), + Icon( + "ic_vue_edu_bookmark", keywords = listOf( + "bookmarks", "save", "favourites", "favorites" + ) + ), ) // endregion // region Files (Vue) private fun vueFiles(): List = listOf( - Icon("ic_vue_files_folder_favorite"), - Icon("ic_vue_files_folder"), - Icon("ic_vue_files_folder_cloud"), + Icon( + "ic_vue_files_folder_favorite", keywords = listOf( + "bookmarks", "save", "favourites", "favorites", "folders", "files folder", "store", + "storage" + ) + ), + Icon( + "ic_vue_files_folder", keywords = listOf( + "bookmarks", "save", "folders", "files folder", "store", "storage" + ) + ), + Icon( + "ic_vue_files_folder_cloud", keywords = listOf( + "bookmarks", "save", "clouds", "folders", "files folder", "store", "storage" + ) + ), ) // endregion // region Location (Vue) private fun vueLocation(): List = listOf( - Icon("ic_vue_location_map1"), - Icon("ic_vue_location_map"), - Icon("ic_vue_location_location"), - Icon("ic_vue_location_global"), - Icon("ic_vue_location_global_search"), - Icon("ic_vue_location_routing"), - Icon("ic_vue_location_discover"), - Icon("ic_vue_location_radar"), - Icon("ic_vue_location_global_edit"), + Icon( + "ic_vue_location_map1", keywords = listOf( + "maps", "atlas", "geography", "traveling", "world", "locations", "places" + ) + ), + Icon( + "ic_vue_location_map", keywords = listOf( + "maps", "atlas", "geography", "traveling", "world", "locations", "places" + ) + ), + Icon( + "ic_vue_location_location", keywords = listOf( + "maps", "atlas", "geography", "traveling", "world", "locations", "gps", "live", "places" + ) + ), + Icon( + "ic_vue_location_global", keywords = listOf( + "global", "globes", "spheres", "world", "webs", "balls", "basketball" + ) + ), + Icon( + "ic_vue_location_global_search", keywords = listOf( + "global", "globes", "spheres", "world", "webs", "searching" + ) + ), + Icon( + "ic_vue_location_routing", keywords = listOf( + "routing", "routs", "locations", "places", "directions", "gps", "maps" + ) + ), + Icon( + "ic_vue_location_discover", keywords = listOf( + "discovering", "locations", "places" + ) + ), + Icon( + "ic_vue_location_radar", keywords = listOf( + "radars", "detection", "cars", "speeds" + ) + ), + Icon( + "ic_vue_location_global_edit", keywords = listOf( + "global", "globes", "spheres", "world", "webs", "editing" + ) + ), ) // endregion // region Main (Vue) private fun vueMain(): List = listOf( - Icon("ic_vue_main_cake"), - Icon("ic_vue_main_reserve"), - Icon("ic_vue_main_archive"), - Icon("ic_vue_main_signpost"), - Icon("ic_vue_main_coffee"), - Icon("ic_vue_main_sport"), - Icon("ic_vue_main_notification"), - Icon("ic_vue_main_lamp_charge"), - Icon("ic_vue_main_home"), - Icon("ic_vue_main_judge"), - Icon("ic_vue_main_timer"), - Icon("ic_vue_main_lamp"), - Icon("ic_vue_main_battery_charging"), - Icon("ic_vue_main_calendar"), - Icon("ic_vue_main_home_wifi"), - Icon("ic_vue_main_tree"), - Icon("ic_vue_main_battery_half"), - Icon("ic_vue_main_send"), - Icon("ic_vue_main_glass"), - Icon("ic_vue_main_emoji_normal"), - Icon("ic_vue_main_share"), - Icon("ic_vue_main_trash"), - Icon("ic_vue_main_milk"), - Icon("ic_vue_main_lifebuoy"), - Icon("ic_vue_main_broom"), - Icon("ic_vue_main_gift"), - Icon("ic_vue_main_clock"), - Icon("ic_vue_main_emoji_happy"), - Icon("ic_vue_main_home_safe"), - Icon("ic_vue_main_crown"), - Icon("ic_vue_main_cup"), - Icon("ic_vue_main_emoji_sad"), - Icon("ic_vue_main_pet"), - Icon("ic_vue_main_flash"), + Icon( + "ic_vue_main_cake", keywords = listOf( + "birthdays", "cakes", "candles", "surprises", "bdays", "b-day" + ) + ), + Icon( + "ic_vue_main_reserve", keywords = listOf( + "reserve", "foods", "reservations", "bells", "hotels", "gourmet", "ringing" + ) + ), + Icon("ic_vue_main_archive", keywords = listOf("archives", "history")), + Icon("ic_vue_main_signpost", keywords = listOf("signposts", "signs", "directions")), + Icon( + "ic_vue_main_coffee", keywords = listOf( + "coffees", "cafes", "hot", "mornings", "wake up", "energy", "drinks", "fun", "cups", + "mugs", "glasses", "warm" + ) + ), + Icon( + "ic_vue_main_sport", keywords = listOf( + "fitness", "gym", "workout", "train", "weights", "sports", "lifting", "dumbbells", + "workouts", "work out" + ) + ), + Icon( + "ic_vue_main_notification", keywords = listOf( + "bells", "ringing", "churches", "notifications", "news" + ) + ), + Icon( + "ic_vue_main_lamp_charge", keywords = listOf( + "lamps", "bulbs", "charge", "charging", "electricity", "flashes", "sparks" + ) + ), + Icon( + "ic_vue_main_home", keywords = listOf( + "homes", "houses", "locations", "live", "remotely" + ) + ), + Icon( + "ic_vue_main_judge", keywords = listOf( + "judges", "lawyers", "legal", "businesses", "institutions", "courthouses" + ) + ), + Icon( + "ic_vue_main_timer", keywords = listOf( + "timer", "clocks", "hourglass", "sandglass" + ) + ), + Icon( + "ic_vue_main_lamp", keywords = listOf( + "lamps", "bulbs", "interiors", "lights", "lighting" + ) + ), + Icon( + "ic_vue_main_battery_charging", keywords = listOf( + "charge", "charging", "electricity", "flashes", "sparks" + ) + ), + Icon( + "ic_vue_main_calendar", keywords = listOf( + "planners", "logbooks", "calendars", "organizers", "organisers", "appointments", + "diary", "notes", "notebooks", "schedules", "agenda" + ) + ), + Icon( + "ic_vue_main_home_wifi", keywords = listOf( + "wifi", "wi-fi", "homes", "networks", "nets", "webs" + ) + ), + Icon( + "ic_vue_main_tree", keywords = listOf( + "trees", "gardens", "yards", "lawns", "woods", "christmas", "xmas", "forests" + ) + ), + Icon("ic_vue_main_battery_half", keywords = listOf("battery", "half", "charges")), + Icon( + "ic_vue_main_send", keywords = listOf( + "sending", "messages", "communication", "chatting", "chats", "play", "telegram", + "sharing", "share" + ) + ), + Icon( + "ic_vue_main_glass", keywords = listOf( + "sunglasses", "eyesight", "vision", "see", "vr", "3d" + ) + ), + Icon( + "ic_vue_main_emoji_normal", keywords = listOf( + "emojis", "normal", "happy", "chill", "joyful", "cheerful", "happiness", "good", + "faces", "emoticon", "emotions", "moods", "vibes" + ) + ), + Icon( + "ic_vue_main_share", keywords = listOf( + "shares", "businesses", "sharing", "company", "structures", "markets", "economy", + "economics", "relations", "communications", "community", "communicate", "groups", + "exchanges" + ) + ), + Icon( + "ic_vue_main_trash", keywords = listOf( + "trashes", "garbages", "junk", "rubbish", "dirt", "useless", "bin", "shit" + ) + ), + Icon( + "ic_vue_main_milk", keywords = listOf( + "milks", "bottles", "glasses", "plastic", "water", "drinks" + ) + ), + Icon( + "ic_vue_main_lifebuoy", keywords = listOf( + "lifebuoy", + "swimming pools", + "seaside", + "ocean", + "save", + "rescue", + "rescuing", + "saveguards", + "rescuers", + "life savers", + "saviors", + "beaches", + "safety", + "safeguards", + "lifeguards" + ) + ), + Icon( + "ic_vue_main_broom", keywords = listOf( + "brooms", "cleaning", "dust", "dirt", "trashes", "garbages", "sweeping", "floors", + "chores", "cleaners", "cleaning woman", "cleaning service", "home duties", "duty", + "brooming" + ) + ), + Icon( + "ic_vue_main_gift", keywords = listOf( + "gifts", "party", "celebrate", "celebrations", "presents", "donations", "donates", + "birthdays", "bdays", "b-day", "holidays" + ) + ), + Icon( + "ic_vue_main_clock", keywords = listOf( + "timer", "clocks", "appointments", "expiration", "expires", "passes", "quickly", + "alarms", "watches", "minutes", "hours", "arrows" + ) + ), + Icon( + "ic_vue_main_emoji_happy", keywords = listOf( + "emojis", "happy", "chill", "joyful", "cheerful", "happiness", "good", + "faces", "emoticon", "emotions", "moods", "vibes" + ) + ), + Icon( + "ic_vue_main_home_safe", keywords = listOf( + "safety", "homes", "houses", "insurances" + ) + ), + Icon( + "ic_vue_main_crown", keywords = listOf( + "crowns", "luxury", "vip", "top", "queens", "kings", "the best", "luxe" + ) + ), + Icon( + "ic_vue_main_cup", keywords = listOf( + "cups", "champions", "victory", "victories", "win", "prizes", "rewards", "awards" + ) + ), + Icon( + "ic_vue_main_emoji_sad", keywords = listOf( + "sad", "bad", "faces", "emoticon", "emotions", "moods", "vibes", "sick", "joyless", + "unhappy" + ) + ), + Icon("ic_vue_main_pet", keywords = listOf("pets", "dogs", "paws", "cats")), + Icon( + "ic_vue_main_flash", keywords = listOf( + "zeus", "lightning", "rains", "emergency", + "urgents", "storms", "flash", "thunders", "important", "priority", "thor", "sparks" + ) + ), ) // endregion // region Media (Vue) private fun vueMedia(): List = listOf( - Icon("ic_vue_media_microphone"), - Icon("ic_vue_media_music"), - Icon("ic_vue_media_voice"), - Icon("ic_vue_media_image"), - Icon("ic_vue_media_scissors"), - Icon("ic_vue_media_mountains"), - Icon("ic_vue_media_film"), - Icon("ic_vue_media_photocamera"), - Icon("ic_vue_media_film_play"), - Icon("ic_vue_media_camera"), - Icon("ic_vue_media_screenmirroring"), - Icon("ic_vue_media_speaker"), - Icon("ic_vue_media_play"), - Icon("ic_vue_media_subtitle"), - Icon("ic_vue_media_setting"), + Icon( + "ic_vue_media_microphone", keywords = listOf( + "microphones", "records", "recordings", "singers", "songs", "singing", "performing", + "tunes" + ) + ), + Icon( + "ic_vue_media_music", keywords = listOf( + "music", "sounds", "spotify", "singers", "songs", "hear", "fun", "party", "records", + "directing", "radios", "produce", "production", "hits", "tunes", "performing", "notes", + "listening", "recordings" + ) + ), + Icon( + "ic_vue_media_voice", keywords = listOf( + "music", "sounds", "singers", "songs", "hear", "records", "directing", "radios", + "produce", "production", "tunes", "performing", "notes", "listening", + "recordings", "voices", "speaking", "talking", "communication", "communicate" + ) + ), + Icon( + "ic_vue_media_image", keywords = listOf( + "images", "pics", "pictures", "gallery", "photos", "editing", "images", "photography" + ) + ), + Icon( + "ic_vue_media_scissors", keywords = listOf( + "scissors", "designers", "cutting", "tools", "cropping", "videos", "editing", + "video editing" + ) + ), + Icon( + "ic_vue_media_mountains", keywords = listOf( + "hike", "hikings", "mountains", "walks", "sun", "tops", "nature", "hobby", "forests", + "woods", "trees", "environments", "sports" + ) + ), + Icon( + "ic_vue_media_film", keywords = listOf( + "films", "movies", "cameras", "videos", "editing", "records", "directing", "studios", + "shows", "tvs", "streams", "acts", "actions", "produces", "productions", "acting", + "vlogs", "vlogging" + ) + ), + Icon( + "ic_vue_media_photocamera", keywords = listOf( + "cameras", "photos", "photography", "pics", "pictures", "images", "editing" + ) + ), + Icon( + "ic_vue_media_film_play", keywords = listOf( + "cameras", "videos", "editing", "films", "movies", "play", "records", "directing", + "studios", "shows", "tvs", "streams", "acts", "actions", "produces", "productions", + "acting", "vlogs", "vlogging" + ) + ), + Icon( + "ic_vue_media_camera", keywords = listOf( + "cameras", "videos", "editing", "photos", "movies", "records", "directing", "studios", + "shows", "tvs", "streams", "acts", "actions", "produces", "productions", "acting", + "films", "vlogs", "vlogging" + ) + ), + Icon( + "ic_vue_media_screenmirroring", keywords = listOf( + "screens", "mirrors", "screen mirroring", "remotely", "screen sharing", "share screen", + "meeting", "presentation" + ) + ), + Icon( + "ic_vue_media_speaker", keywords = listOf( + "speakers", "volume", "sounds", "drivers", "tunes", "listening", "talking", "speaking" + ) + ), + Icon("ic_vue_media_play", keywords = listOf("play", "start", "youtube")), + Icon( + "ic_vue_media_subtitle", keywords = listOf( + "subtitles", "texts", "articles", "chatting", "chats", "texting", "scripts" + ) + ), + Icon( + "ic_vue_media_setting", keywords = listOf( + "settings", "volume", "brightness", "editing", "contrasts", "changes", "changing" + ) + ), ) // endregion // region Messages (Vue) private fun vueMessages(): List = listOf( - Icon("ic_vue_messages_msg_favorite"), - Icon("ic_vue_messages_direct"), - Icon("ic_vue_messages_msg_notification"), - Icon("ic_vue_messages_device_msg"), - Icon("ic_vue_messages_edit"), - Icon("ic_vue_messages_msgs"), - Icon("ic_vue_messages_msg_text"), - Icon("ic_vue_messages_letter"), - Icon("ic_vue_messages_msg"), - Icon("ic_vue_messages_msg_search"), + Icon( + "ic_vue_messages_msg_favorite", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "favorites", + "favourites", "online", "love", "couples", "partnerships", "friendships", "friends", + "hearts", "partners", "sending", "family", "message box", "text box", "texting", + "bubbles", "typing", "comments" + ) + ), + Icon( + "ic_vue_messages_direct", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", "direct", + "couples", "partnerships", "friendships", "friends", "partners", "sending", "family", + "texting" + ) + ), + Icon( + "ic_vue_messages_msg_notification", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", + "notifications", "couples", "partnerships", "friendships", "friends", "circles", + "partners", "sending", "notify", "seen", "news", "missed", "family", "message box", + "text box", "texting", "bubbles" + ) + ), + Icon( + "ic_vue_messages_device_msg", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", + "notifications", "couples", "partnerships", "friendships", "friends", "partners", + "sending", "notify", "seen", "news", "missed", "family", "pc", "computers", + "message box", "text box", "texting", "bubbles", "devices", "typing", "comments" + ) + ), + Icon( + "ic_vue_messages_edit", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", + "editing", "pencils", "pens", "redactions", "texting" + ) + ), + Icon( + "ic_vue_messages_msgs", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", + "notifications", "couples", "partnerships", "friendships", "friends", "partners", + "sending", "notify", "seen", "news", "missed", "family", "message box", "text box", + "texting", "bubbles" + ) + ), + Icon( + "ic_vue_messages_msg_text", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", + "notifications", "couples", "partnerships", "friendships", "friends", "partners", + "sending", "notify", "seen", "news", "missed", "family", "message box", "text box", + "texting", "bubbles", "comments" + ) + ), + Icon( + "ic_vue_messages_letter", keywords = listOf( + "letters", "mails", "watches", "clocks", "ticks", "check marks", "bags", "news", + "missed", "notifications", "communication", "communicate", "online", "working" + ) + ), + Icon( + "ic_vue_messages_msg", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", + "notifications", "couples", "partnerships", "friendships", "friends", "partners", + "sending", "notify", "seen", "news", "missed", "family", "message box", "text box", + "texting", "bubbles", "typing", "comments" + ) + ), + Icon( + "ic_vue_messages_msg_search", keywords = listOf( + "messages", "chatting", "chats", "communication", "communicate", "online", + "notifications", "couples", "partnerships", "friendships", "friends", "partners", + "news", "missed", "family", "message box", "text box", "texting", "bubbles", "comments", + "searching" + ) + ), ) // endregion @@ -1273,37 +1781,37 @@ private fun vuePeople(): List = listOf( // region Security (Vue) private fun vueSecurity(): List = listOf( - Icon("ic_vue_security_eye"), - Icon("ic_vue_security_shield_security"), - Icon("ic_vue_security_key"), - Icon("ic_vue_security_alarm"), - Icon("ic_vue_security_lock"), - Icon("ic_vue_security_password"), - Icon("ic_vue_security_radar"), - Icon("ic_vue_security_shield_person"), - Icon("ic_vue_security_shield"), + Icon("ic_vue_security_eye", keywords=listOf("security", "eye", "vision", "sensor")), + Icon("ic_vue_security_shield_security", keywords=listOf("security", "shield", "protect", "sensor")), + Icon("ic_vue_security_key", keywords=listOf("security", "key")), + Icon("ic_vue_security_alarm", keywords=listOf("security", "alarm", "sensor")), + Icon("ic_vue_security_lock", keywords=listOf("security", "lock", "sensor")), + Icon("ic_vue_security_password", keywords=listOf("security", "password", "pass")), + Icon("ic_vue_security_radar", keywords=listOf("security", "radar", "sensor")), + Icon("ic_vue_security_shield_person", keywords=listOf("security", "shield", "person")), + Icon("ic_vue_security_shield", keywords=listOf("security", "shield", "sensor")), ) // endregion // region Shop (Vue) private fun vueShop(): List = listOf( - Icon("ic_vue_shop_cart"), - Icon("ic_vue_shop_bag"), - Icon("ic_vue_shop_barcode"), - Icon("ic_vue_shop_bag1"), - Icon("ic_vue_shop_shop"), + Icon("ic_vue_shop_cart", keywords=listOf("shop", "cart")), + Icon("ic_vue_shop_bag", keywords=listOf("shop", "bag")), + Icon("ic_vue_shop_barcode", keywords=listOf("shop", "barcode", "code")), + Icon("ic_vue_shop_bag1", keywords=listOf("shop", "bag")), + Icon("ic_vue_shop_shop", keywords=listOf("shop", "options")), ) // endregion // region Support (Vue) private fun vueSupport(): List = listOf( - Icon("ic_vue_support_star"), - Icon("ic_vue_support_medal"), - Icon("ic_vue_support_dislike"), - Icon("ic_vue_support_like_dislike"), - Icon("ic_vue_support_smileys"), - Icon("ic_vue_support_heart"), - Icon("ic_vue_support_like"), + Icon("ic_vue_support_star", keywords=listOf("support", "star", "support reaction", "reaction")), + Icon("ic_vue_support_medal", keywords=listOf("support", "medal", "support reaction", "reaction")), + Icon("ic_vue_support_dislike", keywords=listOf("support", "dislike", "support reaction", "reaction")), + Icon("ic_vue_support_like_dislike", keywords=listOf("support", "like", "dislike", "like dislike", "support reaction", "reaction")), + Icon("ic_vue_support_smileys", keywords=listOf("support", "smileys", "support reaction", "reaction")), + Icon("ic_vue_support_heart", keywords=listOf("support", "heart", "support reaction", "reaction")), + Icon("ic_vue_support_like", keywords=listOf("support", "like", "support reaction", "reaction")), ) // endregion diff --git a/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerModal.kt index de065c2351..56d0792b15 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerModal.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerModal.kt @@ -1,12 +1,9 @@ package com.ivy.core.ui.icon.picker -import androidx.activity.compose.BackHandler -import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.runtime.* @@ -14,13 +11,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.core.ui.R @@ -35,17 +29,14 @@ import com.ivy.data.ItemIconId import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.color.rememberDynamicContrast import com.ivy.design.l1_buildingBlocks.* -import com.ivy.design.l2_components.input.InputFieldType -import com.ivy.design.l2_components.input.IvyInputField import com.ivy.design.l2_components.modal.IvyModal import com.ivy.design.l2_components.modal.Modal import com.ivy.design.l2_components.modal.components.Choose -import com.ivy.design.l2_components.modal.components.Secondary +import com.ivy.design.l2_components.modal.components.Search +import com.ivy.design.l2_components.modal.components.SearchButton import com.ivy.design.l2_components.modal.components.Title -import com.ivy.design.l2_components.modal.scope.ModalActionsScope -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling import com.ivy.design.util.IvyPreview -import com.ivy.design.util.hiltViewmodelPreviewSafe +import com.ivy.design.util.hiltViewModelPreviewSafe import com.ivy.design.util.thenIf private val iconSize = IconSize.M @@ -55,11 +46,12 @@ private val iconPadding = 12.dp @Composable fun BoxScope.IconPickerModal( modal: IvyModal, + level: Int = 1, initialIcon: ItemIcon?, color: Color, onIconPick: (ItemIconId) -> Unit ) { - val viewModel: IconPickerViewModel? = hiltViewmodelPreviewSafe() + val viewModel: IconPickerViewModel? = hiltViewModelPreviewSafe() val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() var selectedIcon by remember(initialIcon) { mutableStateOf(initialIcon?.iconId()) } @@ -74,92 +66,44 @@ fun BoxScope.IconPickerModal( Modal( modal = modal, + level = level, actions = { - ModalActions( - searchBarVisible = searchBarVisible, - showSearch = { searchBarVisible = true }, - resetSearch = resetSearch, - onSelect = { - selectedIcon?.let(onIconPick) - keyboardController?.hide() - modal.hide() - } - ) - } - ) { - Box(modifier = Modifier.weight(1f)) { - LazyColumn( - modifier = Modifier.fillMaxSize() - ) { - item(key = "ic_picker_title") { - this@Modal.Title(text = stringResource(R.string.choose_icon)) - } - sections( - sections = state.sections, - selectedIcon = selectedIcon, - color = color, - onIconSelect = { selectedIcon = it } - ) - item(key = "ic_picker_last_spacer") { SpacerVer(height = 48.dp) } + SearchButton(searchBarVisible = searchBarVisible) { + if (searchBarVisible) resetSearch() else searchBarVisible = true } - SearchBar( - visible = searchBarVisible, - query = state.searchQuery, - resetSearch = resetSearch, - onSearch = { viewModel?.onEvent(IconPickerEvent.Search(it)) } - ) - } - } -} - -// region Header -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun SearchBar( - visible: Boolean, - query: String, - resetSearch: () -> Unit, - onSearch: (String) -> Unit, -) { - AnimatedVisibility( - modifier = Modifier - .fillMaxWidth() - .background(UI.colors.pure) - .padding(top = 16.dp, bottom = 8.dp), - visible = visible, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - IvyInputField( - modifier = Modifier - .focusRequester(focusRequester) - .fillMaxWidth() - .padding(horizontal = 16.dp), - type = InputFieldType.SingleLine, - initialValue = query, - placeholder = "Search by words (car, home, tech)", - imeAction = ImeAction.Search, - onImeAction = { + SpacerHor(width = 8.dp) + Choose { + selectedIcon?.let(onIconPick) keyboardController?.hide() - focusRequester.freeFocus() - }, - onValueChange = { onSearch(it) }, - ) - - LaunchedEffect(visible) { - if (visible) { - focusRequester.requestFocus() - keyboardController?.show() + modal.hide() } } - BackHandler(enabled = visible) { - resetSearch() + ) { + Search( + searchBarVisible = searchBarVisible, + initialSearchQuery = state.searchQuery, + searchHint = "Search by words (car, home, tech)", + resetSearch = resetSearch, + onSearch = { viewModel?.onEvent(IconPickerEvent.Search(it)) }, + ) { + item(key = "ic_picker_title") { + this@Modal.Title(text = stringResource(R.string.choose_icon)) + } + sections( + sections = state.sections, + selectedIcon = selectedIcon, + color = color, + onIconSelect = { + selectedIcon = it + onIconPick(it) + keyboardController?.hide() + modal.hide() + } + ) + item(key = "ic_picker_last_spacer") { SpacerVer(height = 48.dp) } } } } -// endregion private fun LazyListScope.sections( sections: List, @@ -284,29 +228,6 @@ private fun IconItem( // endregion -// region Modal Actions -@Composable -private fun ModalActionsScope.ModalActions( - searchBarVisible: Boolean, - resetSearch: () -> Unit, - showSearch: () -> Unit, - onSelect: () -> Unit, -) { - Secondary( - text = null, - icon = if (searchBarVisible) - R.drawable.round_search_off_24 else R.drawable.round_search_24, - feeling = if (searchBarVisible) ButtonFeeling.Negative else ButtonFeeling.Positive - ) { - // toggle search bar - if (searchBarVisible) resetSearch() else showSearch() - } - SpacerHor(width = 8.dp) - Choose(onClick = onSelect) -} -// endregion - - // region Preview @Preview @Composable diff --git a/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerViewModel.kt index 57dbb1aadc..19ed7fe32d 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerViewModel.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/icon/picker/IconPickerViewModel.kt @@ -1,6 +1,6 @@ package com.ivy.core.ui.icon.picker -import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.SimpleFlowViewModel import com.ivy.core.domain.pure.ui.groupByRows import com.ivy.core.ui.action.ItemIconOptionalAct import com.ivy.core.ui.icon.picker.data.Icon @@ -18,21 +18,19 @@ import javax.inject.Inject @HiltViewModel internal class IconPickerViewModel @Inject constructor( private val itemIconOptionalAct: ItemIconOptionalAct -) : FlowViewModel() { +) : SimpleFlowViewModel() { companion object { const val ICONS_PER_ROW = 4 } - override fun initialState(): IconPickerStateUi = IconPickerStateUi( + override val initialUi = IconPickerStateUi( sections = emptyList(), searchQuery = "" ) - override fun initialUiState(): IconPickerStateUi = initialState() - private val searchQuery = MutableStateFlow("") - override fun stateFlow(): Flow = sectionsUiFlow().map { sections -> + override val uiFlow: Flow = sectionsUiFlow().map { sections -> IconPickerStateUi( sections = sections, searchQuery = searchQuery.value @@ -48,13 +46,12 @@ internal class IconPickerViewModel @Inject constructor( if (itemIcons.isNotEmpty()) { SectionUi( name = section.name, - iconRows = groupByRows(itemIcons, iconsPerRow = ICONS_PER_ROW), + iconRows = groupByRows(itemIcons, itemsPerRow = ICONS_PER_ROW), ) } else null } } - @OptIn(FlowPreview::class) private fun sectionsFlow(): Flow> = searchQuery .debounce(100) @@ -79,7 +76,6 @@ internal class IconPickerViewModel @Inject constructor( // Icon must have at least one keyword that contains the search query icon.keywords.any { keyword -> keyword.contains(query) } - override suspend fun mapToUiState(state: IconPickerStateUi): IconPickerStateUi = state // region Event Handling override suspend fun handleEvent(event: IconPickerEvent) = when (event) { diff --git a/core/ui/src/main/java/com/ivy/core/ui/modals/RateModal.kt b/core/ui/src/main/java/com/ivy/core/ui/modals/RateModal.kt new file mode 100644 index 0000000000..bb6b8dfbb3 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/modals/RateModal.kt @@ -0,0 +1,70 @@ +package com.ivy.core.ui.modals + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.amount.AmountModal +import com.ivy.data.Value +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.H2Second +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.RateModal( + modal: IvyModal, + rate: Double, + fromCurrency: String, + toCurrency: String, + level: Int = 1, + key: String = "default", + onRateChange: (Double) -> Unit, +) { + AmountModal( + modal = modal, + level = level, + key = key, + contentAbove = { + SpacerVer(height = 24.dp) + H2Second( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = "$fromCurrency to $toCurrency", + color = UI.colors.primary, + textAlign = TextAlign.Center, + ) + SpacerVer(height = 24.dp) + }, + initialAmount = Value( + amount = rate, + currency = "", + ), + onAmountEnter = { + onRateChange(it.amount) + }, + ) +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + RateModal( + modal = modal, + rate = 1.95, + fromCurrency = "EUR", + toCurrency = "BGN", + onRateChange = {}, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/GlobalProvider.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/GlobalProvider.kt index 2ef92e7e20..4ae78ecdf2 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/GlobalProvider.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/temp/GlobalProvider.kt @@ -1,9 +1,7 @@ package com.ivy.core.ui.temp import android.content.Context -import com.ivy.base.RootIntent object GlobalProvider { - lateinit var rootIntent: RootIntent lateinit var appContext: Context } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/IvyAndroidAppExt.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/IvyAndroidAppExt.kt index f27e07ef44..ac98375a06 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/IvyAndroidAppExt.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/temp/IvyAndroidAppExt.kt @@ -6,6 +6,7 @@ import android.content.ComponentName import android.content.Intent import androidx.annotation.StringRes +@Deprecated("use @Compose stringResource() or appContext.getString()") fun stringRes( @StringRes id: Int, vararg args: String diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/BudgetExt.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/trash/BudgetExt.kt deleted file mode 100644 index 2b1342704c..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/BudgetExt.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.ivy.core.ui.temp.trash - -import com.ivy.base.R -import com.ivy.core.ui.temp.stringRes -import com.ivy.data.Budget -import java.util.* - -object BudgetExt { - fun serialize(ids: List): String { - return ids.joinToString(separator = ",") - } - - fun type(categoriesCount: Int): String { - return when (categoriesCount) { - 0 -> stringRes(R.string.total_budget) - 1 -> stringRes(R.string.category_budget) - else -> stringRes( - R.string.multi_category_budget, - categoriesCount.toString() - ) - } - } -} - - -fun Budget.parseCategoryIds(): List { - return parseIdsString(categoryIdsSerialized) -} - -fun Budget.parseAccountIds(): List { - return parseIdsString(accountIdsSerialized) -} - -private fun Budget.parseIdsString(idsString: String?): List { - return try { - if (idsString == null) return emptyList() - - idsString - .split(",") - .map { UUID.fromString(it) } - } catch (e: Exception) { - e.printStackTrace() - emptyList() - } -} - - -fun Budget.validate(): Boolean { - return name.isNotEmpty() && amount > 0.0 -} diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/CustomerJourneyCardData.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/trash/CustomerJourneyCardData.kt deleted file mode 100644 index 0703b0be85..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/CustomerJourneyCardData.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.ivy.core.ui.temp.trash - -import androidx.annotation.DrawableRes -import com.ivy.design.l0_system.color.Gradient - -data class CustomerJourneyCardData( - val id: String, - val condition: (trnCount: Long, plannedPaymentsCount: Long, ivyContext: IvyWalletCtx) -> Boolean, - - val title: String, - val description: String, - val cta: String, - @DrawableRes val ctaIcon: Int, - - val hasDismiss: Boolean = true, - - val background: Gradient, - val onAction: (IvyWalletCtx, com.ivy.core.ui.temp.RootScreen) -> Unit -) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/IvyWalletCtx.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/trash/IvyWalletCtx.kt deleted file mode 100644 index 3be71d3807..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/IvyWalletCtx.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.ivy.core.ui.temp.trash - -import android.net.Uri -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.ivy.base.MainTab -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.design.IvyContext -import java.time.LocalDate -import java.time.LocalTime -import java.util.* - -@Deprecated("don't use, it's bad!") -class IvyWalletCtx : IvyContext() { - //------------------------------------------ State --------------------------------------------- - @Deprecated("don't use, it's bad!") - var startDayOfMonth = 1 - private set - - @Deprecated("don't use, it's bad!") - fun setStartDayOfMonth(day: Int) { - startDayOfMonth = day - } - - //---------------------- Optimization ---------------------------- - @Deprecated("use IvyState") - val categoryMap: MutableMap = mutableMapOf() - - @Deprecated("use IvyState") - val accountMap: MutableMap = mutableMapOf() - //---------------------- Optimization ---------------------------- - - @Deprecated("use IvyState") - var selectedPeriod: TimePeriod = TimePeriod.currentMonth( - startDayOfMonth = startDayOfMonth //this is default value - ) - - @Deprecated("don't use, it's bad!") - private var selectedPeriodInitialized = false - - @Deprecated("use IvyState") - fun initSelectedPeriodInMemory( - startDayOfMonth: Int, - forceReinitialize: Boolean = false - ): TimePeriod { - if (!selectedPeriodInitialized || forceReinitialize) { - selectedPeriod = TimePeriod.currentMonth( - startDayOfMonth = startDayOfMonth - ) - selectedPeriodInitialized = true - } - - return selectedPeriod - } - - @Deprecated("use IvyState") - fun updateSelectedPeriodInMemory(period: TimePeriod) { - selectedPeriod = period - } - - @Deprecated("use IvyState") - var transactionsListState: LazyListState? = null - - @Deprecated("don't use, it's bad!") - var mainTab by mutableStateOf(MainTab.HOME) - private set - - @Deprecated("don't use, it's bad!") - fun selectMainTab(tab: MainTab) { - mainTab = tab - } - - @Deprecated("don't use, it's bad!") - var moreMenuExpanded = false - private set - - @Deprecated("don't use, it's bad!") - fun setMoreMenuExpanded(expanded: Boolean) { - moreMenuExpanded = expanded - } - //------------------------------------------ State --------------------------------------------- - - - //Activity help ------------------------------------------------------------------------------- - @Deprecated("don't use, it's bad!") - lateinit var onShowDatePicker: ( - minDate: LocalDate?, - maxDate: LocalDate?, - initialDate: LocalDate?, - onDatePicked: (LocalDate) -> Unit - ) -> Unit - - @Deprecated("don't use, it's bad!") - lateinit var onShowTimePicker: (onDatePicked: (LocalTime) -> Unit) -> Unit - - @Deprecated("don't use, it's bad!") - fun datePicker( - minDate: LocalDate? = null, - maxDate: LocalDate? = null, - initialDate: LocalDate?, - onDatePicked: (LocalDate) -> Unit - ) { - onShowDatePicker(minDate, maxDate, initialDate, onDatePicked) - } - - @Deprecated("don't use, it's bad!") - fun timePicker(onTimePicked: (LocalTime) -> Unit) { - onShowTimePicker(onTimePicked) - } - //Activity help ------------------------------------------------------------------------------- - - - // Billing ------------------------------------------------------------------------------------- - @Deprecated("don't use, it's bad!") - var isPremium = true //if (BuildConfig.DEBUG) Constants.PREMIUM_INITIAL_VALUE_DEBUG else false - // Billing ------------------------------------------------------------------------------------- - - @Deprecated("don't use, it's bad!") - lateinit var googleSignIn: (idTokenResult: (String?) -> Unit) -> Unit - - @Deprecated("don't use, it's bad!") - lateinit var createNewFile: (fileName: String, onCreated: (Uri) -> Unit) -> Unit - - @Deprecated("don't use, it's bad!") - lateinit var openFile: (onOpened: (Uri) -> Unit) -> Unit - - //Testing -------------------------------------------------------------------------------------- - @Deprecated("don't use, it's bad!") - fun reset() { - mainTab = MainTab.HOME - startDayOfMonth = 1 - isPremium = true - transactionsListState = null - } -} diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/LastNTimeRange.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/trash/LastNTimeRange.kt deleted file mode 100644 index ce3c5d4bb3..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/LastNTimeRange.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.ivy.core.ui.temp.trash - -import com.ivy.base.R -import com.ivy.common.time.timeNow -import com.ivy.data.planned.IntervalType -import java.time.LocalDateTime - -@Deprecated("don't use, it's bad!") -data class LastNTimeRange( - val periodN: Int, - val periodType: IntervalType, -) - -@Deprecated("don't use, it's bad!") -fun LastNTimeRange.fromDate(): LocalDateTime = periodType.incrementDate( - date = timeNow(), - intervalN = -periodN.toLong() -) - -@Deprecated("don't use, it's bad!") -fun LastNTimeRange.forDisplay(): String = - "$periodN ${periodType.forDisplay(periodN)}" - - -@Deprecated("don't use, it's bad!") -fun IntervalType.forDisplay(intervalN: Int): String { - val plural = intervalN > 1 || intervalN == 0 - return when (this) { - IntervalType.DAY -> if (plural) com.ivy.core.ui.temp.stringRes(R.string.days) else com.ivy.core.ui.temp.stringRes( - R.string.day - ) - IntervalType.WEEK -> if (plural) com.ivy.core.ui.temp.stringRes(R.string.weeks) else com.ivy.core.ui.temp.stringRes( - R.string.week - ) - IntervalType.MONTH -> if (plural) com.ivy.core.ui.temp.stringRes(R.string.months) else com.ivy.core.ui.temp.stringRes( - R.string.month - ) - IntervalType.YEAR -> if (plural) com.ivy.core.ui.temp.stringRes(R.string.years) else com.ivy.core.ui.temp.stringRes( - R.string.year - ) - } -} - -@Deprecated("don't use, it's bad!") -fun IntervalType.incrementDate(date: LocalDateTime, intervalN: Long): LocalDateTime { - return when (this) { - IntervalType.DAY -> date.plusDays(intervalN) - IntervalType.WEEK -> date.plusWeeks(intervalN) - IntervalType.MONTH -> date.plusMonths(intervalN) - IntervalType.YEAR -> date.plusYears(intervalN) - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/Month.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/trash/Month.kt deleted file mode 100644 index 179e0b3fb3..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/Month.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.ivy.core.ui.temp.trash - -import com.ivy.base.R -import com.ivy.common.time.dateNowUTC -import com.ivy.core.ui.temp.stringRes -import java.time.LocalDate - -@Deprecated("don't use, it's bad!") -data class Month( - val monthValue: Int, - val name: String -) { - companion object { - @Deprecated("don't use, it's bad!") - fun monthsList(): MutableList = mutableListOf( - Month(1, stringRes(R.string.january)), - Month(2, stringRes(R.string.february)), - Month(3, stringRes(R.string.march)), - Month(4, stringRes(R.string.april)), - Month(5, stringRes(R.string.may)), - Month(6, stringRes(R.string.june)), - Month(7, stringRes(R.string.july)), - Month(8, stringRes(R.string.august)), - Month(9, stringRes(R.string.september)), - Month(10, stringRes(R.string.october)), - Month(11, stringRes(R.string.november)), - Month(12, stringRes(R.string.december)), - ) - - fun fromMonthValue(code: Int): Month = - monthsList().first { it.monthValue == code } - } - - fun toDate(): LocalDate = - dateNowUTC().withMonth(monthValue) - - - fun incrementMonthPeriod( - ivyContext: IvyWalletCtx, - increment: Long, - year: Int - ): TimePeriod { - val incrementedMonth = toDate().withYear(year).plusMonths(increment) - val incrementedPeriod = TimePeriod( - month = fromMonthValue(incrementedMonth.monthValue), - year = incrementedMonth.year - ) - ivyContext.updateSelectedPeriodInMemory(incrementedPeriod) - return incrementedPeriod - } - - fun toTimePeriod(): TimePeriod = TimePeriod( - month = this - ) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/TimePeriod.kt b/core/ui/src/main/java/com/ivy/core/ui/temp/trash/TimePeriod.kt deleted file mode 100644 index 6b10b5d267..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/temp/trash/TimePeriod.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.ivy.core.ui.temp.trash - -import com.ivy.base.FromToTimeRange -import com.ivy.common.time.* -import java.time.LocalDate -import java.time.LocalDateTime - -@Deprecated("Migration to Period") -data class TimePeriod( - val month: com.ivy.core.ui.temp.trash.Month? = null, - val year: Int? = null, - val fromToRange: FromToTimeRange? = null, - val lastNRange: com.ivy.core.ui.temp.trash.LastNTimeRange? = null, -) { - companion object { - /** - * Examples: - * 1. startDateOfMonth = 1, today = Nov. 10 - * return Nov. 1 - Nov. 30 - * - * 2. startDateOfMonth = 10, today = Nov. 9 - * return Oct. 10 - Nov. 9 - * - * 3. startDateOfMonth = 10, today = Nov. 10 - * return Nov. 10 - Dec. 9 - */ - fun currentMonth(startDayOfMonth: Int): TimePeriod { - val dateNowUTC = dateNowUTC() - val dayToday = dateNowUTC.dayOfMonth - - //Examples month = Nov. startDate = 7; Period = from Nov (7) till Dec (6) - // => new period starts if today => startDayOfMonth - val newPeriodStarted = dayToday >= startDayOfMonth - - val periodDate = if (newPeriodStarted) { - //new monthly period has already started then observe it => current month - dateNowUTC - } else { - //new monthly period hasn't yet started then observe the ongoing one => previous month - dateNowUTC.minusMonths(1) - } - - return TimePeriod( - month = Month.fromMonthValue( - periodDate.monthValue - ), - year = periodDate.year - ) - } - } - - fun isValid(): Boolean = - month != null || fromToRange != null || lastNRange != null - - fun toRange( - startDateOfMonth: Int - ): FromToTimeRange { - return when { - month != null -> { - val date = if (year != null) month.toDate().withYear(year) else month.toDate() - val (from, to) = if (startDateOfMonth != 1) { - customStartDayOfMonthPeriodRange( - date = date, - startDateOfMonth = startDateOfMonth - ) - } else { - Pair(startOfMonth(date), endOfMonth(date)) - } - - FromToTimeRange( - from = from, - to = to - ) - } - fromToRange != null -> { - fromToRange - } - lastNRange != null -> { - FromToTimeRange( - from = lastNRange.fromDate(), - to = timeNow() - ) - } - else -> { - val date = dateNowUTC() - FromToTimeRange( - from = startOfMonth(date), - to = endOfMonth(date) - ) - } - } - } - - private fun customStartDayOfMonthPeriodRange( - date: LocalDate, - startDateOfMonth: Int - ): Pair { - val from = date - .withDayOfMonthSafe(startDateOfMonth) - .atStartOfDay() - .convertLocalToUTC() - - val to = date - //startDayOfMonth != 1 just shift N day the month forward so to should +1 month - .plusMonths(1) - .withDayOfMonthSafe(startDateOfMonth) - //e.g. Correct: 14.10-13.11 (Incorrect: 14.10-14.11) - .minusDays(1) - .atEndOfDay() - .convertLocalToUTC() - - return Pair(from, to) - } - - fun toDisplayShort( - startDateOfMonth: Int - ): String { - return when { - month != null -> { - if (startDateOfMonth == 1) { - displayMonthStartingOn1st(month = month) - } else { - val range = toRange(startDateOfMonth) - val pattern = "MMM dd" - "${range.from?.format(pattern)} - ${range.to?.format(pattern)}" - } - } - fromToRange != null -> { - fromToRange.toDisplay() - } - lastNRange != null -> { - "Last ${lastNRange.forDisplay()}" - } - else -> "Custom" - } - } - - fun toDisplayLong( - startDateOfMonth: Int - ): String { - return when { - month != null -> { - if (startDateOfMonth == 1) { - displayMonthStartingOn1st(month = month) - } else { - toRange(startDateOfMonth).toDisplay() - } - } - fromToRange != null -> { - fromToRange.toDisplay() - } - lastNRange != null -> { - "the last ${lastNRange.forDisplay()}" - } - else -> { - toRange(startDateOfMonth).toDisplay() - } - } - } - - private fun displayMonthStartingOn1st(month: com.ivy.core.ui.temp.trash.Month): String { - val year = year - return if (year != null && dateNowUTC().year != year) { - //not this year - "${month.name}, $year" - } else { - //this year - month.name - } - } - -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/FormatTime.kt b/core/ui/src/main/java/com/ivy/core/ui/time/FormatTime.kt index 19ae181da6..9a0a07ec8f 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/time/FormatTime.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/time/FormatTime.kt @@ -1,16 +1,17 @@ package com.ivy.core.ui.time import android.content.Context -import com.ivy.base.R -import com.ivy.common.time.dateNowUTC import com.ivy.common.time.format +import com.ivy.common.time.provider.TimeProvider +import com.ivy.resources.R import java.time.LocalDateTime fun LocalDateTime.formatNicely( context: Context, + timeProvider: TimeProvider, includeWeekDay: Boolean = true, ): String { - val today = dateNowUTC() + val today = timeProvider.dateNow() val isThisYear = today.year == this.year val patternNoWeekDay = "dd MMM" diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/PeriodButton.kt b/core/ui/src/main/java/com/ivy/core/ui/time/PeriodButton.kt index 1e7d4886df..d0edc1c45b 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/time/PeriodButton.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/time/PeriodButton.kt @@ -12,12 +12,12 @@ import com.ivy.core.ui.time.handling.SelectPeriodEvent import com.ivy.core.ui.time.handling.SelectedPeriodViewModel import com.ivy.design.l2_components.modal.IvyModal import com.ivy.design.l2_components.modal.rememberIvyModal -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.ComponentPreview -import com.ivy.design.util.hiltViewmodelPreviewSafe +import com.ivy.design.util.hiltViewModelPreviewSafe import com.ivy.wallet.utils.horizontalSwipeListener @Composable @@ -26,7 +26,7 @@ fun PeriodButton( periodModal: IvyModal, modifier: Modifier = Modifier, ) { - val viewModel: SelectedPeriodViewModel? = hiltViewmodelPreviewSafe() + val viewModel: SelectedPeriodViewModel? = hiltViewModelPreviewSafe() IvyButton( modifier = modifier.horizontalSwipeListener( @@ -41,8 +41,8 @@ fun PeriodButton( } ), size = ButtonSize.Small, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Medium, + feeling = Feeling.Positive, text = selectedPeriod.btnText(), icon = R.drawable.ic_round_calendar_month_24, ) { diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/PeriodModal.kt b/core/ui/src/main/java/com/ivy/core/ui/time/PeriodModal.kt index 5545389ef4..39469ef734 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/time/PeriodModal.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/time/PeriodModal.kt @@ -29,12 +29,12 @@ import com.ivy.design.l2_components.modal.Modal import com.ivy.design.l2_components.modal.components.Done import com.ivy.design.l2_components.modal.components.Title import com.ivy.design.l2_components.modal.rememberIvyModal -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.IvyPreview -import com.ivy.design.util.hiltViewmodelPreviewSafe +import com.ivy.design.util.hiltViewModelPreviewSafe import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -43,7 +43,7 @@ fun BoxScope.PeriodModal( modal: IvyModal, selectedPeriod: SelectedPeriodUi ) { - val viewModel: SelectedPeriodViewModel? = hiltViewmodelPreviewSafe() + val viewModel: SelectedPeriodViewModel? = hiltViewModelPreviewSafe() val state = viewModel?.uiState?.collectAsState()?.value ?: previewState() UI( modal = modal, @@ -162,8 +162,8 @@ private fun MonthItem( ) { IvyButton( size = ButtonSize.Small, - visibility = if (selected) ButtonVisibility.High else ButtonVisibility.Medium, - feeling = if (selected) ButtonFeeling.Positive else ButtonFeeling.Neutral, + visibility = if (selected) Visibility.High else Visibility.Medium, + feeling = if (selected) Feeling.Positive else Feeling.Neutral, text = if (month.currentYear) month.fullName else "${month.fullName}, ${month.year}", icon = null ) { @@ -264,8 +264,8 @@ private fun DateButton( IvyButton( modifier = modifier, size = ButtonSize.Big, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Neutral, + visibility = Visibility.Medium, + feeling = Feeling.Neutral, text = dateText, icon = R.drawable.ic_round_calendar_month_24, onClick = onClick @@ -298,8 +298,8 @@ private fun MoreOptions( modifier = Modifier.weight(1f), size = ButtonSize.Big, visibility = if (selected is SelectedPeriodUi.AllTime) - ButtonVisibility.High else ButtonVisibility.Medium, - feeling = ButtonFeeling.Positive, + Visibility.High else Visibility.Medium, + feeling = Feeling.Positive, text = "All-time", icon = R.drawable.ic_baseline_all_inclusive_24 ) { @@ -309,8 +309,8 @@ private fun MoreOptions( IvyButton( modifier = Modifier.weight(1f), size = ButtonSize.Big, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Negative, + visibility = Visibility.Medium, + feeling = Feeling.Negative, text = "Reset", icon = R.drawable.ic_round_undo_24 ) { @@ -321,8 +321,8 @@ private fun MoreOptions( IvyButton( modifier = Modifier.padding(horizontal = 8.dp), size = ButtonSize.Big, - visibility = ButtonVisibility.Low, - feeling = ButtonFeeling.Neutral, + visibility = Visibility.Low, + feeling = Feeling.Neutral, text = "See more", icon = R.drawable.ic_round_expand_less_24, onClick = onShowMoreOptionsModal @@ -414,8 +414,8 @@ private fun MoreOptionsButton( IvyButton( modifier = Modifier.padding(horizontal = 16.dp), size = ButtonSize.Big, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Medium, + feeling = Feeling.Positive, text = text, icon = null, onClick = onClick diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/handling/SelectedPeriodViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/time/handling/SelectedPeriodViewModel.kt index 45c5ee0525..907ac6b4d6 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/time/handling/SelectedPeriodViewModel.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/time/handling/SelectedPeriodViewModel.kt @@ -4,8 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import androidx.compose.runtime.Immutable import com.ivy.common.time.atEndOfDay -import com.ivy.common.time.dateNowLocal -import com.ivy.common.time.timeNow +import com.ivy.common.time.provider.TimeProvider import com.ivy.core.domain.FlowViewModel import com.ivy.core.domain.action.period.SelectedPeriodFlow import com.ivy.core.domain.action.period.SetSelectedPeriodAct @@ -21,6 +20,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import java.time.LocalDate import javax.inject.Inject @@ -31,9 +31,9 @@ class SelectedPeriodViewModel @Inject constructor( private val appContext: Context, private val startDayOfMonthFlow: StartDayOfMonthFlow, private val selectedPeriodFlow: SelectedPeriodFlow, - private val setSelectedPeriodAct: SetSelectedPeriodAct + private val setSelectedPeriodAct: SetSelectedPeriodAct, + private val timeProvider: TimeProvider, ) : FlowViewModel() { - data class State( val selectedPeriod: SelectedPeriod, val startDayOfMonth: Int, @@ -46,21 +46,21 @@ class SelectedPeriodViewModel @Inject constructor( val months: List, ) - override fun initialState(): State = State( + override val initialState = State( startDayOfMonth = 1, months = emptyList(), selectedPeriod = SelectedPeriod.AllTime(allTime()) ) - override fun initialUiState(): UiState = UiState( + override val initialUi = UiState( startDayOfMonth = 1, months = emptyList(), ) - override fun stateFlow(): Flow = combine( + override val stateFlow: Flow = combine( startDayOfMonthFlow(), selectedPeriodFlow() ) { startDayOfMonth, selectedPeriod -> - val currentYear = dateNowLocal().year + val currentYear = timeProvider.dateNow().year State( startDayOfMonth = startDayOfMonth, @@ -71,26 +71,30 @@ class SelectedPeriodViewModel @Inject constructor( ) } - override suspend fun mapToUiState(state: State): UiState = UiState( - startDayOfMonth = state.startDayOfMonth, - months = state.months - ) + override val uiFlow: Flow = stateFlow.map { + UiState( + startDayOfMonth = it.startDayOfMonth, + months = it.months + ) + } override suspend fun handleEvent(event: SelectPeriodEvent) { val selectedPeriod = when (event) { SelectPeriodEvent.AllTime -> SelectedPeriod.AllTime(allTime()) is SelectPeriodEvent.CustomRange -> SelectedPeriod.CustomRange(event.range) is SelectPeriodEvent.InTheLast -> toSelectedPeriod(event) - is SelectPeriodEvent.Monthly -> dateToSelectedMonthlyPeriod( + is SelectPeriodEvent.Monthly -> monthlyPeriod( // TODO: Refactor that // 10 is a safe date in the middle of the month dateInPeriod = LocalDate.of(event.month.year, event.month.number, 10), startDayOfMonth = state.value.startDayOfMonth ) - SelectPeriodEvent.ResetToCurrentPeriod -> - currentMonthlyPeriod(startDayOfMonth = state.value.startDayOfMonth) - SelectPeriodEvent.LastYear -> yearPeriod(dateNowLocal().year - 1) - SelectPeriodEvent.ThisYear -> yearPeriod(dateNowLocal().year) + SelectPeriodEvent.ResetToCurrentPeriod -> currentMonthlyPeriod( + startDayOfMonth = state.value.startDayOfMonth, + timeProvider = timeProvider, + ) + SelectPeriodEvent.LastYear -> yearlyPeriod(timeProvider.dateNow().year - 1) + SelectPeriodEvent.ThisYear -> yearlyPeriod(timeProvider.dateNow().year) is SelectPeriodEvent.ShiftForward -> shiftPeriodForward() is SelectPeriodEvent.ShiftBackward -> shiftPeriodBackward() } @@ -99,7 +103,7 @@ class SelectedPeriodViewModel @Inject constructor( } private fun toSelectedPeriod(event: SelectPeriodEvent.InTheLast): SelectedPeriod.InTheLast { - val now = timeNow() + val now = timeProvider.timeNow() val n = event.n return SelectedPeriod.InTheLast( n = n, @@ -108,7 +112,7 @@ class SelectedPeriodViewModel @Inject constructor( // n - 1 because we count today // Negate: -n because we want to start from the **last** N unit from = shiftTime(time = now, n = -(n - 1), unit = event.unit), - to = dateNowLocal().atEndOfDay(), + to = timeProvider.dateNow().atEndOfDay(), ) ) } @@ -118,7 +122,7 @@ class SelectedPeriodViewModel @Inject constructor( is SelectedPeriod.AllTime -> SelectedPeriod.AllTime(allTime()) is SelectedPeriod.CustomRange -> shiftPeriod(selected.range, ShiftDirection.Forward) is SelectedPeriod.InTheLast -> shiftPeriod(selected.range, ShiftDirection.Forward) - is SelectedPeriod.Monthly -> dateToSelectedMonthlyPeriod( + is SelectedPeriod.Monthly -> monthlyPeriod( dateInPeriod = selected.range.from.toLocalDate() .plusMonths(1), startDayOfMonth = state.value.startDayOfMonth @@ -130,7 +134,7 @@ class SelectedPeriodViewModel @Inject constructor( is SelectedPeriod.AllTime -> SelectedPeriod.AllTime(allTime()) is SelectedPeriod.CustomRange -> shiftPeriod(selected.range, ShiftDirection.Backward) is SelectedPeriod.InTheLast -> shiftPeriod(selected.range, ShiftDirection.Backward) - is SelectedPeriod.Monthly -> dateToSelectedMonthlyPeriod( + is SelectedPeriod.Monthly -> monthlyPeriod( dateInPeriod = selected.range.from.toLocalDate() .minusMonths(1), startDayOfMonth = state.value.startDayOfMonth diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/component/HorizontalWheelPicker.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/component/HorizontalWheelPicker.kt new file mode 100644 index 0000000000..5ae6814c03 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/component/HorizontalWheelPicker.kt @@ -0,0 +1,131 @@ +package com.ivy.core.ui.time.picker.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B1Second +import com.ivy.design.l1_buildingBlocks.SpacerHor + +private val ITEM_HEIGHT = 64.dp +private val ITEM_WIDTH = 104.dp +private val SELECTOR_LINES_PADDING_FROM_CENTER = 16.dp +private val SELECTOR_LINE_WIDTH = 2.dp + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HorizontalWheelPicker( + items: List, + initialIndex: Int, + itemsCount: Int, + text: (T) -> String, + modifier: Modifier = Modifier, + onSelectedChange: (T) -> Unit, +) { + val listState = rememberLazyListState() + val selectedIndex by remember { + derivedStateOf { + (listState.firstVisibleItemIndex) + .coerceIn(0 until itemsCount) + } + } + LaunchedEffect(selectedIndex) { + onSelectedChange(items[selectedIndex.coerceIn(0 until itemsCount)]) + } + + LaunchedEffect(initialIndex) { + listState.scrollToItem(index = initialIndex) + } + + var selectIndexOnClick by remember { + // skip first spacer + // skip first item + // => select the 2nd (center item) + mutableStateOf(null) + } + LaunchedEffect(selectIndexOnClick) { + selectIndexOnClick?.let { + listState.animateScrollToItem(it) + // reset, so the same index can be re-selected + selectIndexOnClick = null + } + } + + val primary = UI.colors.primary + LazyRow( + modifier = modifier + .width(3 * ITEM_WIDTH) + .drawWithCache { + onDrawBehind { + val halfItem = ITEM_WIDTH.value / 2 + val padding = SELECTOR_LINES_PADDING_FROM_CENTER.toPx() + val lineWidth = SELECTOR_LINE_WIDTH.toPx() + drawLine( + start = Offset(x = center.x - halfItem - padding, y = 0f), + end = Offset(x = center.x - halfItem - padding, y = size.height), + strokeWidth = lineWidth, + color = primary, + cap = StrokeCap.Butt + ) + drawLine( + start = Offset(x = center.x + halfItem + padding, y = 0f), + end = Offset(x = center.x + halfItem + padding, y = size.height), + strokeWidth = lineWidth, + color = primary, + cap = StrokeCap.Butt + ) + } + }, + state = listState, + flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) + ) { + item(key = "space_zero") { + SpacerHor(width = ITEM_WIDTH) + } + itemsIndexed( + items = items, + key = { index, _ -> index } + ) { index, item -> + Box( + modifier = Modifier + .defaultMinSize(minHeight = ITEM_HEIGHT) + .width(ITEM_WIDTH) + .clip(UI.shapes.squared) + .clickable { + selectIndexOnClick = index + }, + contentAlignment = Alignment.Center + ) { + val selected = index == selectedIndex + B1Second( + text = text(item), + fontWeight = FontWeight.Bold, + color = if (selected) + UI.colors.primary else UI.colorsInverted.pure, + textAlign = TextAlign.Center, + maxLines = 1 + ) + } + } + item(key = "space_last") { + SpacerHor(width = ITEM_WIDTH) + } + } +} diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/component/VerticalWheelPicker.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/component/VerticalWheelPicker.kt new file mode 100644 index 0000000000..eccd625422 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/component/VerticalWheelPicker.kt @@ -0,0 +1,131 @@ +package com.ivy.core.ui.time.picker.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B1Second +import com.ivy.design.l1_buildingBlocks.SpacerVer + +private val ITEM_HEIGHT = 72.dp +private val ITEM_WIDTH = 104.dp +private val SELECTOR_LINES_PADDING_FROM_CENTER = 16.dp +private val SELECTOR_LINE_WIDTH = 2.dp + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun VerticalWheelPicker( + items: List, + initialIndex: Int, + itemsCount: Int, + text: (T) -> String, + modifier: Modifier = Modifier, + onSelectedChange: (T) -> Unit, +) { + val listState = rememberLazyListState() + val selectedIndex by remember { + derivedStateOf { + (listState.firstVisibleItemIndex) + .coerceIn(0 until itemsCount) + } + } + LaunchedEffect(selectedIndex) { + onSelectedChange(items[selectedIndex.coerceIn(0 until itemsCount)]) + } + + LaunchedEffect(initialIndex) { + listState.scrollToItem(index = initialIndex) + } + + var selectIndexOnClick by remember { + // skip first spacer + // skip first item + // => select the 2nd (center item) + mutableStateOf(null) + } + LaunchedEffect(selectIndexOnClick) { + selectIndexOnClick?.let { + listState.animateScrollToItem(it) + // reset, so the same index can be re-selected + selectIndexOnClick = null + } + } + + val primary = UI.colors.primary + LazyColumn( + modifier = modifier + .height(3 * ITEM_HEIGHT) + .drawWithCache { + onDrawBehind { + val halfItem = ITEM_HEIGHT.value / 2 + val padding = SELECTOR_LINES_PADDING_FROM_CENTER.toPx() + val lineWidth = SELECTOR_LINE_WIDTH.toPx() + drawLine( + start = Offset(x = 0f, y = center.y - halfItem - padding), + end = Offset(x = size.width, y = center.y - halfItem - padding), + strokeWidth = lineWidth, + color = primary, + cap = StrokeCap.Butt + ) + drawLine( + start = Offset(x = 0f, y = center.y + halfItem + padding), + end = Offset(x = size.width, y = center.y + halfItem + padding), + strokeWidth = lineWidth, + color = primary, + cap = StrokeCap.Butt + ) + } + }, + state = listState, + flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) + ) { + item(key = "space_zero") { + SpacerVer(height = ITEM_HEIGHT) + } + itemsIndexed( + items = items, + key = { index, _ -> index } + ) { index, item -> + Box( + modifier = Modifier + .defaultMinSize(minWidth = ITEM_WIDTH) + .height(ITEM_HEIGHT) + .clip(UI.shapes.squared) + .clickable { + selectIndexOnClick = index + }, + contentAlignment = Alignment.Center + ) { + val selected = index == selectedIndex + B1Second( + text = text(item), + fontWeight = FontWeight.Bold, + color = if (selected) + UI.colors.primary else UI.colorsInverted.pure, + textAlign = TextAlign.Center, + maxLines = 1 + ) + } + } + item(key = "space_last") { + SpacerVer(height = ITEM_HEIGHT) + } + } +} diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerEvent.kt new file mode 100644 index 0000000000..dc4151a03a --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerEvent.kt @@ -0,0 +1,13 @@ +package com.ivy.core.ui.time.picker.date + +import com.ivy.core.ui.time.picker.date.data.PickerDay +import com.ivy.core.ui.time.picker.date.data.PickerMonth +import com.ivy.core.ui.time.picker.date.data.PickerYear +import java.time.LocalDate + +sealed interface DatePickerEvent { + data class Initial(val selected: LocalDate) : DatePickerEvent + data class DayChange(val day: PickerDay) : DatePickerEvent + data class MonthChange(val month: PickerMonth) : DatePickerEvent + data class YearChange(val year: PickerYear) : DatePickerEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerModal.kt new file mode 100644 index 0000000000..d8e33a36ac --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerModal.kt @@ -0,0 +1,189 @@ +package com.ivy.core.ui.time.picker.date + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.time.picker.component.HorizontalWheelPicker +import com.ivy.core.ui.time.picker.component.VerticalWheelPicker +import com.ivy.core.ui.time.picker.date.data.PickerDay +import com.ivy.core.ui.time.picker.date.data.PickerMonth +import com.ivy.core.ui.time.picker.date.data.PickerYear +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B2Second +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l1_buildingBlocks.SpacerWeight +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l2_components.modal.scope.ModalScope +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import java.time.LocalDate + +@Composable +fun BoxScope.DatePickerModal( + modal: IvyModal, + selected: LocalDate, + level: Int = 1, + contentTop: @Composable ModalScope.() -> Unit = {}, + onPick: (LocalDate) -> Unit, +) { + val viewModel: DatePickerViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel = viewModel, preview = ::previewState) + + LaunchedEffect(selected) { + viewModel?.onEvent(DatePickerEvent.Initial(selected)) + } + + Modal( + modal = modal, + level = level, + actions = { + Positive(text = "Choose") { + onPick(state.selected) + modal.hide() + } + } + ) { + Title(text = "Pick a date") + contentTop() + SpacerVer(height = 24.dp) + B2Second( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = state.selectedContext, + color = UI.colors.primary, + fontWeight = FontWeight.Bold, + ) + SpacerVer(height = 24.dp) + YearWheel( + modifier = Modifier.align(Alignment.CenterHorizontally), + years = state.years, + yearsCount = state.yearsListSize, + initialYearValue = selected.year, + onYearChange = { viewModel?.onEvent(DatePickerEvent.YearChange(it)) } + ) + SpacerVer(height = 16.dp) + Row { + SpacerWeight(weight = 1f) + DayWheel( + days = state.days, + daysCount = state.daysListSize, + initialDayValue = selected.dayOfMonth - 1, + onDayChange = { viewModel?.onEvent(DatePickerEvent.DayChange(it)) } + ) + MonthWheel( + months = state.months, + monthsCount = state.monthsListSize, + initialMonthValue = selected.monthValue - 1, + onMonthChange = { viewModel?.onEvent(DatePickerEvent.MonthChange(it)) } + ) + SpacerWeight(weight = 1f) + } + SpacerVer(height = 24.dp) + } +} + +@Composable +private fun DayWheel( + days: List, + daysCount: Int, + initialDayValue: Int, + modifier: Modifier = Modifier, + onDayChange: (PickerDay) -> Unit, +) { + VerticalWheelPicker( + modifier = modifier, + items = days, + itemsCount = daysCount, + initialIndex = initialDayValue, + text = { it.text }, + onSelectedChange = onDayChange + ) +} + +@Composable +private fun MonthWheel( + months: List, + monthsCount: Int, + initialMonthValue: Int, + modifier: Modifier = Modifier, + onMonthChange: (PickerMonth) -> Unit, +) { + VerticalWheelPicker( + modifier = modifier, + items = months, + itemsCount = monthsCount, + initialIndex = initialMonthValue, + text = { it.text }, + onSelectedChange = onMonthChange + ) +} + +@Composable +private fun YearWheel( + years: List, + yearsCount: Int, + initialYearValue: Int, + modifier: Modifier = Modifier, + onYearChange: (PickerYear) -> Unit, +) { + HorizontalWheelPicker( + modifier = modifier, + items = years, + itemsCount = yearsCount, + initialIndex = initialYearValue - (years.firstOrNull()?.value ?: 0), + text = { it.text }, + onSelectedChange = onYearChange + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + DatePickerModal( + modal = modal, + selected = LocalDate.now(), + onPick = {}, + ) + } +} + +private fun previewState() = DatePickerState( + days = listOf( + PickerDay("1", 1), + PickerDay("2", 2), + PickerDay("3", 3), + PickerDay("4", 4), + ), + daysListSize = 3, + months = listOf( + PickerMonth("Jan", 1), + PickerMonth("Feb", 2), + PickerMonth("Mar", 3), + PickerMonth("Apr", 4), + ), + monthsListSize = 3, + years = listOf( + PickerYear("2020", 2020), + PickerYear("2021", 2021), + PickerYear("2022", 2022), + PickerYear("2023", 2023), + ), + yearsListSize = 3, + selectedContext = "Today", + selected = LocalDate.now(), +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerState.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerState.kt new file mode 100644 index 0000000000..dd55e29190 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerState.kt @@ -0,0 +1,20 @@ +package com.ivy.core.ui.time.picker.date + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.time.picker.date.data.PickerDay +import com.ivy.core.ui.time.picker.date.data.PickerMonth +import com.ivy.core.ui.time.picker.date.data.PickerYear +import java.time.LocalDate + +@Immutable +data class DatePickerState( + val days: List, + val daysListSize: Int, + val months: List, + val monthsListSize: Int, + val years: List, + val yearsListSize: Int, + + val selectedContext: String, + val selected: LocalDate, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerViewModel.kt new file mode 100644 index 0000000000..526afb9a4d --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/DatePickerViewModel.kt @@ -0,0 +1,106 @@ +package com.ivy.core.ui.time.picker.date + +import android.annotation.SuppressLint +import android.content.Context +import com.ivy.common.time.contextText +import com.ivy.common.time.provider.TimeProvider +import com.ivy.common.time.withDayOfMonthSafe +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.ui.time.picker.date.data.PickerDay +import com.ivy.core.ui.time.picker.date.data.PickerMonth +import com.ivy.core.ui.time.picker.date.data.PickerYear +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@SuppressLint("StaticFieldLeak") +@HiltViewModel +class DatePickerViewModel @Inject constructor( + @ApplicationContext private val appContext: Context, + timeProvider: TimeProvider +) : SimpleFlowViewModel() { + companion object { + const val YEARS_FROM_NOW_SUPPORT = 100 + } + + override val initialUi = DatePickerState( + days = emptyList(), + daysListSize = 0, + months = emptyList(), + monthsListSize = 0, + years = emptyList(), + yearsListSize = 0, + selectedContext = "Today", + selected = timeProvider.dateNow() + ) + + private val selectedDate = MutableStateFlow(initialUi.selected) + + override val uiFlow: Flow = selectedDate.map { selected -> + val days = (1..selected.month.maxLength()).map { PickerDay(it.toString(), it) } + val months = listOf( + PickerMonth("Jan", 1), + PickerMonth("Feb", 2), + PickerMonth("Mar", 3), + PickerMonth("Apr", 4), + PickerMonth("May", 5), + PickerMonth("Jun", 6), + PickerMonth("Jul", 7), + PickerMonth("Aug", 8), + PickerMonth("Sep", 9), + PickerMonth("Oct", 10), + PickerMonth("Nov", 11), + PickerMonth("Dec", 12), + ) + + val currentYear = timeProvider.dateNow().year + val years = (currentYear - YEARS_FROM_NOW_SUPPORT.. + currentYear + YEARS_FROM_NOW_SUPPORT) + .map { + PickerYear(it.toString(), it) + } + + DatePickerState( + days = days, + daysListSize = days.size, + months = months, + monthsListSize = months.size, + years = years, + yearsListSize = years.size, + selectedContext = selected.contextText( + alwaysShowWeekday = true, + getString = appContext::getString + ), + selected = selected, + ) + } + + + // region Event Handling + override suspend fun handleEvent(event: DatePickerEvent) = when (event) { + is DatePickerEvent.Initial -> handleInitial(event) + is DatePickerEvent.DayChange -> handleDayChange(event) + is DatePickerEvent.MonthChange -> handleMonthChange(event) + is DatePickerEvent.YearChange -> handleYearChange(event) + } + + private fun handleInitial(event: DatePickerEvent.Initial) { + selectedDate.value = event.selected + } + + private fun handleDayChange(event: DatePickerEvent.DayChange) { + selectedDate.value = selectedDate.value.withDayOfMonthSafe(event.day.value) + } + + private fun handleMonthChange(event: DatePickerEvent.MonthChange) { + selectedDate.value = selectedDate.value.withMonth(event.month.value) + } + + private fun handleYearChange(event: DatePickerEvent.YearChange) { + selectedDate.value = selectedDate.value.withYear(event.year.value) + } + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerDay.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerDay.kt new file mode 100644 index 0000000000..f3dbba5483 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerDay.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.time.picker.date.data + +import androidx.compose.runtime.Immutable + +@Immutable +data class PickerDay( + val text: String, + val value: Int, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerMonth.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerMonth.kt new file mode 100644 index 0000000000..c3681d5e5c --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerMonth.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.time.picker.date.data + +import androidx.compose.runtime.Immutable + +@Immutable +data class PickerMonth( + val text: String, + val value: Int, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerYear.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerYear.kt new file mode 100644 index 0000000000..2c164b705f --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/date/data/PickerYear.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.time.picker.date.data + +import androidx.compose.runtime.Immutable + +@Immutable +data class PickerYear( + val text: String, + val value: Int, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerEvent.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerEvent.kt new file mode 100644 index 0000000000..15fb33597b --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerEvent.kt @@ -0,0 +1,12 @@ +package com.ivy.core.ui.time.picker.time + +import com.ivy.core.ui.time.picker.time.data.PickerHour +import com.ivy.core.ui.time.picker.time.data.PickerMinute +import java.time.LocalTime + +sealed interface TimePickerEvent { + data class Initial(val initialTime: LocalTime) : TimePickerEvent + data class HourChange(val pickerHour: PickerHour) : TimePickerEvent + data class MinuteChange(val minute: PickerMinute) : TimePickerEvent + data class AmPmChange(val amPm: AmPm) : TimePickerEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerModal.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerModal.kt new file mode 100644 index 0000000000..78615a7e74 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerModal.kt @@ -0,0 +1,185 @@ +package com.ivy.core.ui.time.picker.time + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.time.picker.component.HorizontalWheelPicker +import com.ivy.core.ui.time.picker.component.VerticalWheelPicker +import com.ivy.core.ui.time.picker.time.data.PickerHour +import com.ivy.core.ui.time.picker.time.data.PickerMinute +import com.ivy.core.ui.uiStatePreviewSafe +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.H2Second +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l1_buildingBlocks.SpacerWeight +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l2_components.modal.scope.ModalScope +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.hiltViewModelPreviewSafe +import java.time.LocalTime + +@Composable +fun BoxScope.TimePickerModal( + modal: IvyModal, + selected: LocalTime, + level: Int = 1, + contentTop: @Composable ModalScope.() -> Unit = {}, + onPick: (LocalTime) -> Unit, +) { + val viewModel: TimePickerViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel = viewModel, preview = ::previewState) + + LaunchedEffect(selected) { + viewModel?.onEvent(TimePickerEvent.Initial(selected)) + } + + Modal( + modal = modal, + level = level, + actions = { + Positive(text = "Choose") { + onPick(state.selected) + modal.hide() + } + } + ) { + Title(text = "Pick a time") + contentTop() + SpacerVer(height = 24.dp) + if (state.amPm != null) { + AmPmWheel( + modifier = Modifier.align(Alignment.CenterHorizontally), + initialAmPmValue = state.amPm, + onAmPmChange = { + viewModel?.onEvent(TimePickerEvent.AmPmChange(it)) + } + ) + SpacerVer(height = 16.dp) + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + SpacerWeight(weight = 1f) + HoursWheel( + hours = state.hours, + hoursCount = state.hoursListSize, + initialHourIndex = state.selectedHourIndex, + onHourChange = { viewModel?.onEvent(TimePickerEvent.HourChange(it)) } + ) + SpacerHor(width = 12.dp) + H2Second( + text = ":", + fontWeight = FontWeight.Bold, + color = UI.colors.primary, + ) + SpacerHor(width = 12.dp) + MinuteWheel( + minutes = state.minutes, + minutesCount = state.minutesListSize, + initialMinute = selected.minute, + onMinuteChange = { viewModel?.onEvent(TimePickerEvent.MinuteChange(it)) } + ) + SpacerWeight(weight = 1f) + } + SpacerVer(height = 24.dp) + } +} + +@Composable +private fun HoursWheel( + hours: List, + hoursCount: Int, + initialHourIndex: Int, + modifier: Modifier = Modifier, + onHourChange: (PickerHour) -> Unit, +) { + VerticalWheelPicker( + modifier = modifier, + items = hours, + itemsCount = hoursCount, + initialIndex = initialHourIndex, + text = { it.text }, + onSelectedChange = onHourChange + ) +} + +@Composable +private fun MinuteWheel( + minutes: List, + minutesCount: Int, + initialMinute: Int, + modifier: Modifier = Modifier, + onMinuteChange: (PickerMinute) -> Unit, +) { + VerticalWheelPicker( + modifier = modifier, + items = minutes, + itemsCount = minutesCount, + initialIndex = initialMinute, + text = { it.text }, + onSelectedChange = onMinuteChange + ) +} + +@Composable +private fun AmPmWheel( + initialAmPmValue: AmPm, + modifier: Modifier = Modifier, + onAmPmChange: (AmPm) -> Unit, +) { + HorizontalWheelPicker( + modifier = modifier, + items = listOf( + AmPm.AM to "AM", + AmPm.PM to "PM", + AmPm.AM to "AM", + AmPm.PM to "PM", + ), + itemsCount = 4, + initialIndex = when (initialAmPmValue) { + AmPm.AM -> 0 + AmPm.PM -> 1 + }, + text = { it.second }, + onSelectedChange = { (amPm, _) -> + onAmPmChange(amPm) + } + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + TimePickerModal( + modal = modal, + selected = LocalTime.now(), + onPick = {}, + ) + } +} + +private fun previewState() = TimePickerState( + amPm = AmPm.PM, + hours = (1..11).map { PickerHour(it.toString().padStart(2, '0'), it) }, + hoursListSize = 1, + minutes = (0..59).map { PickerMinute(it.toString().padStart(2, '0'), it) }, + minutesListSize = 60, + selected = LocalTime.now(), + selectedHourIndex = LocalTime.now().hour, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerState.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerState.kt new file mode 100644 index 0000000000..bb1f6397a1 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerState.kt @@ -0,0 +1,24 @@ +package com.ivy.core.ui.time.picker.time + +import androidx.compose.runtime.Immutable +import com.ivy.core.ui.time.picker.time.data.PickerHour +import com.ivy.core.ui.time.picker.time.data.PickerMinute +import java.time.LocalTime + +@Immutable +data class TimePickerState( + val amPm: AmPm?, + val hours: List, + val hoursListSize: Int, + val minutes: List, + val minutesListSize: Int, + + val selectedHourIndex: Int, + val selected: LocalTime, +) + +@Immutable +enum class AmPm { + AM, + PM, +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerViewModel.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerViewModel.kt new file mode 100644 index 0000000000..b9c9d58266 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/TimePickerViewModel.kt @@ -0,0 +1,154 @@ +package com.ivy.core.ui.time.picker.time + +import android.annotation.SuppressLint +import android.content.Context +import com.ivy.common.time.provider.TimeProvider +import com.ivy.common.time.uses24HourFormat +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.ui.time.picker.time.data.PickerHour +import com.ivy.core.ui.time.picker.time.data.PickerMinute +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +/** + * https://en.wikipedia.org/wiki/12-hour_clock + */ +@SuppressLint("StaticFieldLeak") +@HiltViewModel +class TimePickerViewModel @Inject constructor( + @ApplicationContext private val appContext: Context, + timeProvider: TimeProvider +) : SimpleFlowViewModel() { + // TODO: AM/PM <-> 24h logic is too complex and messy! Consider refactoring! + // TODO: Very shitty code!!! + + override val initialUi = TimePickerState( + amPm = null, + hours = emptyList(), + hoursListSize = 0, + minutes = emptyList(), + minutesListSize = 0, + selected = timeProvider.timeNow().toLocalTime(), + selectedHourIndex = 0, + ) + + private val amPm = MutableStateFlow(initialUi.amPm) + private val selected = MutableStateFlow(initialUi.selected) + private val initialSelected = MutableStateFlow(initialUi.selected) + + override val uiFlow: Flow = combine( + selected, + initialSelected, + amPm + ) { selected24, initialSelected, amPm -> + val hours = when (amPm) { + AmPm.AM -> 1..12 + AmPm.PM -> 1..11 + null -> 0..23 + }.map { + PickerHour( + value = it, + text = it.toString().padStart(2, '0'), + ) + } + val minutes = (0..59).map { + PickerMinute( + value = it, + text = it.toString().padStart(2, '0'), + ) + } + + TimePickerState( + amPm = amPm, + hours = hours, + hoursListSize = hours.size, + minutes = minutes, + minutesListSize = minutes.size, + selected = selected24, + selectedHourIndex = when (amPm) { + AmPm.AM -> { + /* + Possible values: 1..12 + Indexes: 0..11 + */ + (if (initialSelected.hour == 0) 12 else initialSelected.hour) - 1 + // -1 because indexes start from 0 and AM/PM doesn't! + } + AmPm.PM -> { + /* + Possible values: 1..11 + Indexes: 0..10 + */ + (initialSelected.hour % 12) - 1 // -1 because indexes start from 0 and AM/PM doesn't! + } + null -> { + /* + Possible values: 0..23 + Indexes: 0..23 + */ + initialSelected.hour + } + }.coerceIn(0..hours.lastIndex), + ) + } + + + // region Event Handling + override suspend fun handleEvent(event: TimePickerEvent) = when (event) { + is TimePickerEvent.Initial -> handleInitial(event) + is TimePickerEvent.HourChange -> handleHourChange(event) + is TimePickerEvent.AmPmChange -> handleAmPmChange(event) + is TimePickerEvent.MinuteChange -> handleMinuteChange(event) + } + + private fun handleInitial(event: TimePickerEvent.Initial) { + updateAmPm(event.initialTime.hour) + initialSelected.value = event.initialTime + selected.value = event.initialTime + } + + private fun handleHourChange(event: TimePickerEvent.HourChange) { + val pickedHour = event.pickerHour.value + + // transform the input picker hour to 24h format (0-23) + val newHour24 = when (amPm.value) { + AmPm.AM -> if (pickedHour == 12) 0 else pickedHour + AmPm.PM -> pickedHour + 12 + null -> pickedHour + } + + updateHour(newHour24) + } + + private fun updateAmPm(newHour24: Int): AmPm? { + val newAmPm = if (!uses24HourFormat(appContext)) { + if (newHour24 < 12) AmPm.AM else AmPm.PM + } else null + amPm.value = newAmPm + return newAmPm + } + + private fun handleAmPmChange(event: TimePickerEvent.AmPmChange) { + amPm.value = event.amPm + val newHour24 = when (event.amPm) { + AmPm.AM -> if (selected.value.hour == 0) 12 else selected.value.hour + AmPm.PM -> selected.value.hour + 12 + } + updateHour(newHour24) + initialSelected.value = selected.value + } + + private fun updateHour(newHour24: Int) { + selected.value = selected.value.withHour(newHour24.coerceIn(0..23)) + } + + private fun handleMinuteChange(event: TimePickerEvent.MinuteChange) { + selected.value = selected.value.withMinute(event.minute.value) + } + + // endregion +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/data/PickerHour.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/data/PickerHour.kt new file mode 100644 index 0000000000..85aea5efee --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/data/PickerHour.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.time.picker.time.data + +import androidx.compose.runtime.Immutable + +@Immutable +data class PickerHour( + val text: String, + val value: Int, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/data/PickerMinute.kt b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/data/PickerMinute.kt new file mode 100644 index 0000000000..8377ebce1f --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/time/picker/time/data/PickerMinute.kt @@ -0,0 +1,9 @@ +package com.ivy.core.ui.time.picker.time.data + +import androidx.compose.runtime.Immutable + +@Immutable +data class PickerMinute( + val text: String, + val value: Int, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/DateDivider.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/DateDivider.kt index 55203aed3a..0e75f23015 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/DateDivider.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/DateDivider.kt @@ -1,12 +1,19 @@ package com.ivy.core.ui.transaction +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.* +import androidx.compose.ui.graphics.Path import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.ivy.common.time.dateId +import com.ivy.common.time.dateNowLocal +import com.ivy.core.domain.action.calculate.transaction.toggleTrnListDate import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.domain.pure.format.dummyValueUi import com.ivy.core.ui.data.transaction.TrnListItemUi @@ -14,19 +21,44 @@ import com.ivy.design.l0_system.UI import com.ivy.design.l1_buildingBlocks.B1 import com.ivy.design.l1_buildingBlocks.B2Second import com.ivy.design.l1_buildingBlocks.Caption -import com.ivy.design.l1_buildingBlocks.SpacerVer import com.ivy.design.util.ComponentPreview +import com.ivy.design.util.thenIf @Composable -fun TrnListItemUi.DateDivider.DateDivider() { +fun DateDivider(divider: TrnListItemUi.DateDivider) { + val primary = UI.colors.primary Row( modifier = Modifier - .padding(start = 24.dp, end = 32.dp) - .fillMaxWidth(), + .fillMaxWidth() + .clickable { + toggleTrnListDate(dateId = divider.id) + } + .thenIf(divider.collapsed) { + drawBehind { + val cornerRadius = CornerRadius(12.dp.toPx(), 12.dp.toPx()) + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect( + offset = Offset.Zero, + size = Size( + width = 8.dp.toPx(), + height = size.height + ), + ), + topRight = cornerRadius, + bottomRight = cornerRadius, + ) + ) + } + drawPath(path, color = primary) + } + } + .padding(start = 24.dp, end = 32.dp), verticalAlignment = Alignment.CenterVertically ) { - Date(date = date, day = day) - Cashflow(cashflow = cashflow, positiveCashflow = positiveCashflow) + Date(date = divider.date, day = divider.day) + Cashflow(cashflow = divider.cashflow, positiveCashflow = divider.positiveCashflow) } } @@ -39,7 +71,6 @@ private fun RowScope.Date( modifier = Modifier.weight(1f) ) { B1(text = date, fontWeight = FontWeight.ExtraBold) - SpacerVer(height = 4.dp) Caption(text = day, fontWeight = FontWeight.Bold) } } @@ -55,17 +86,22 @@ private fun Cashflow( ) } + // region Previews @Preview @Composable private fun Preview_Positive() { ComponentPreview { - TrnListItemUi.DateDivider( - date = "September 25.", - day = "Today", - cashflow = dummyValueUi("154.32"), - positiveCashflow = true - ).DateDivider() + DateDivider( + TrnListItemUi.DateDivider( + id = dateNowLocal().dateId(), + date = "September 25.", + day = "Today", + cashflow = dummyValueUi("154.32"), + positiveCashflow = true, + collapsed = false, + ) + ) } } @@ -73,12 +109,16 @@ private fun Preview_Positive() { @Composable private fun Preview_Negative() { ComponentPreview { - TrnListItemUi.DateDivider( - date = "September 25. 2020", - day = "Today", - cashflow = dummyValueUi("-1k"), - positiveCashflow = false - ).DateDivider() + DateDivider( + TrnListItemUi.DateDivider( + id = dateNowLocal().dateId(), + date = "September 25. 2020", + day = "Today", + cashflow = dummyValueUi("-1k"), + positiveCashflow = false, + collapsed = true, + ) + ) } } // endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/DueSectionDivider.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/DueSectionDivider.kt index 9c7af31297..8b13121725 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/DueSectionDivider.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/DueSectionDivider.kt @@ -1,6 +1,7 @@ package com.ivy.core.ui.transaction import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -16,7 +17,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.ivy.base.R import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.domain.pure.format.dummyValueUi import com.ivy.core.ui.data.transaction.DueSectionUi @@ -30,8 +30,8 @@ import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.design.l1_buildingBlocks.SpacerVer import com.ivy.design.l3_ivyComponents.IvyDividerDot import com.ivy.design.util.ComponentPreview -import com.ivy.design.util.clickableNoIndication import com.ivy.design.util.springBounce +import com.ivy.resources.R @Composable fun DueSectionUi.SectionDivider( @@ -41,7 +41,7 @@ fun DueSectionUi.SectionDivider( Row( modifier = Modifier .fillMaxWidth() - .clickableNoIndication { + .clickable { setExpanded(!expanded) }, verticalAlignment = Alignment.CenterVertically diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionTypeExt.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionTypeExt.kt new file mode 100644 index 0000000000..ad2b283dc1 --- /dev/null +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionTypeExt.kt @@ -0,0 +1,26 @@ +package com.ivy.core.ui.transaction + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.ivy.data.transaction.TransactionType +import com.ivy.design.l0_system.color.Green +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.resources.R + +@Composable +fun TransactionType.humanText(): String = when (this) { + TransactionType.Income -> stringResource(R.string.income) + TransactionType.Expense -> stringResource(R.string.expense) +} + +@DrawableRes +fun TransactionType.icon(): Int = when (this) { + TransactionType.Income -> R.drawable.ic_income + TransactionType.Expense -> R.drawable.ic_expense +} + +fun TransactionType.feeling(): Feeling = when (this) { + TransactionType.Income -> Feeling.Custom(Green) + TransactionType.Expense -> Feeling.Negative +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsLazyColumn.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsLazyColumn.kt index 6c3ccfc1dc..e7860f4472 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsLazyColumn.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsLazyColumn.kt @@ -4,14 +4,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.common.time.timeNow import com.ivy.core.domain.pure.format.dummyValueUi -import com.ivy.core.ui.data.dummyAccountUi +import com.ivy.core.ui.data.account.dummyAccountUi import com.ivy.core.ui.data.dummyCategoryUi import com.ivy.core.ui.data.icon.dummyIconSized import com.ivy.core.ui.data.icon.dummyIconUnknown @@ -61,6 +60,7 @@ fun TransactionsLazyColumn( upcomingHandler: ExpandCollapseHandler = defaultExpandCollapseHandler(), overdueHandler: ExpandCollapseHandler = defaultExpandCollapseHandler(), trnItemClickHandler: TrnItemClickHandler = defaultTrnItemClickHandler(), + onFirstVisibleItemChange: (suspend (Int) -> Unit)? = null, ) { val state = rememberLazyListState( initialFirstVisibleItemIndex = @@ -69,6 +69,16 @@ fun TransactionsLazyColumn( lazyStateCache[scrollStateKey]?.firstVisibleItemScrollOffset ?: 0 ) + if (onFirstVisibleItemChange != null) { + val firstVisibleItemIndex by remember { + derivedStateOf { state.firstVisibleItemIndex } + } + + LaunchedEffect(firstVisibleItemIndex) { + onFirstVisibleItemChange(firstVisibleItemIndex) + } + } + if (scrollStateKey != null) { // Cache scrolling state DisposableEffect(key1 = scrollStateKey) { @@ -122,21 +132,23 @@ fun sampleTransactionListUi(): TransactionsListUi = TransactionsListUi( income = dummyValueUi("16.99"), expense = null, trns = listOf( - dummyTransactionUi( - title = "Upcoming payment", - account = dummyAccountUi( - name = "Revolut", - color = Purple, - icon = dummyIconSized(R.drawable.ic_custom_revolut_s) - ), - category = dummyCategoryUi( - name = "Investments", - color = Blue2Light, - icon = dummyIconSized(R.drawable.ic_custom_leaf_s) - ), - value = dummyValueUi("16.99"), - type = TransactionType.Income, - time = dummyTrnTimeDueUi(timeNow().plusDays(1)) + TrnListItemUi.Trn( + dummyTransactionUi( + title = "Upcoming payment", + account = dummyAccountUi( + name = "Revolut", + color = Purple, + icon = dummyIconSized(R.drawable.ic_custom_revolut_s) + ), + category = dummyCategoryUi( + name = "Investments", + color = Blue2Light, + icon = dummyIconSized(R.drawable.ic_custom_leaf_s) + ), + value = dummyValueUi("16.99"), + type = TransactionType.Income, + time = dummyTrnTimeDueUi(timeNow().plusDays(1)) + ) ) ) ), @@ -145,26 +157,30 @@ fun sampleTransactionListUi(): TransactionsListUi = TransactionsListUi( income = null, expense = dummyValueUi("650.0"), trns = listOf( - dummyTransactionUi( - title = "Rent", - value = dummyValueUi("650.0"), - account = dummyAccountUi( - name = "Cash", - color = Green, - icon = dummyIconUnknown(R.drawable.ic_vue_money_coins) - ), - category = null, - type = TransactionType.Expense, - time = dummyTrnTimeDueUi() + TrnListItemUi.Trn( + dummyTransactionUi( + title = "Rent", + value = dummyValueUi("650.0"), + account = dummyAccountUi( + name = "Cash", + color = Green, + icon = dummyIconUnknown(R.drawable.ic_vue_money_coins) + ), + category = null, + type = TransactionType.Expense, + time = dummyTrnTimeDueUi() + ) ) ) ), history = listOf( TrnListItemUi.DateDivider( + id = "2021-01-01", date = "September 25.", day = "Friday", cashflow = dummyValueUi("-30.0"), - positiveCashflow = false + positiveCashflow = false, + collapsed = false, ), TrnListItemUi.Trn( dummyTransactionUi( @@ -185,10 +201,12 @@ fun sampleTransactionListUi(): TransactionsListUi = TransactionsListUi( ) ), TrnListItemUi.DateDivider( + id = "2021-01-01", date = "September 23.", day = "Wednesday", cashflow = dummyValueUi("105.33"), - positiveCashflow = true + positiveCashflow = true, + collapsed = false, ), TrnListItemUi.Trn( dummyTransactionUi( diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsList.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsList.kt index 35b87d749d..b6f6846c66 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsList.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/TransactionsList.kt @@ -17,13 +17,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.ivy.core.ui.data.transaction.DueSectionUi -import com.ivy.core.ui.data.transaction.TransactionUi import com.ivy.core.ui.data.transaction.TransactionsListUi import com.ivy.core.ui.data.transaction.TrnListItemUi -import com.ivy.core.ui.transaction.card.Card import com.ivy.core.ui.transaction.card.DueActions +import com.ivy.core.ui.transaction.card.TransactionCard +import com.ivy.core.ui.transaction.card.TransferCard import com.ivy.core.ui.transaction.card.dummyDueActions import com.ivy.core.ui.transaction.handling.ExpandCollapseHandler import com.ivy.core.ui.transaction.handling.TrnItemClickHandler @@ -62,12 +63,16 @@ internal fun LazyListScope.transactionsList( ) { dueSection( section = trnsList.upcoming, + key = "upcoming_section", + paddingTop = 20.dp, handler = upcomingHandler, trnClickHandler = trnClickHandler, dueActions = dueActions ) dueSection( section = trnsList.overdue, + key = "overdue_section", + paddingTop = 20.dp, handler = overdueHandler, trnClickHandler = trnClickHandler, dueActions = dueActions @@ -85,13 +90,15 @@ internal fun LazyListScope.transactionsList( private fun LazyListScope.dueSection( section: DueSectionUi?, + key: String, + paddingTop: Dp, handler: ExpandCollapseHandler, trnClickHandler: TrnItemClickHandler, dueActions: DueActions?, ) { if (section != null) { - item { - SpacerVer(height = 24.dp) + item(key = key) { + SpacerVer(height = paddingTop) section.SectionDivider( expanded = handler.expanded, setExpanded = handler.setExpanded, @@ -108,22 +115,41 @@ private fun LazyListScope.dueSection( } private fun LazyListScope.dueTrns( - trns: List, + trns: List, trnClickHandler: TrnItemClickHandler, dueActions: DueActions?, ) { items( items = trns, - key = { it.id } - ) { trn -> + key = { + when (it) { + is TrnListItemUi.DateDivider -> "impossible-${it.date}" + is TrnListItemUi.Transfer -> it.batchId + is TrnListItemUi.Trn -> it.trn.id + } + } + ) { item -> SpacerVer(height = 12.dp) - trn.Card( - modifier = Modifier.padding(horizontal = 16.dp), - onClick = trnClickHandler.onTrnClick, - onAccountClick = trnClickHandler.onAccountClick, - onCategoryClick = trnClickHandler.onCategoryClick, - dueActions = dueActions - ) + when (item) { + is TrnListItemUi.DateDivider -> {} // date dividers can't be in due, should not happen + is TrnListItemUi.Transfer -> TransferCard( + modifier = Modifier.padding(horizontal = 16.dp), + transfer = item, + onClick = trnClickHandler.onTransferClick, + onAccountClick = trnClickHandler.onAccountClick, + onCategoryClick = trnClickHandler.onCategoryClick, + dueActions = dueActions, + ) + is TrnListItemUi.Trn -> TransactionCard( + modifier = Modifier.padding(horizontal = 16.dp), + trn = item.trn, + onClick = trnClickHandler.onTrnClick, + onAccountClick = trnClickHandler.onAccountClick, + onCategoryClick = trnClickHandler.onCategoryClick, + dueActions = dueActions + ) + } + } } @@ -135,7 +161,7 @@ private fun LazyListScope.history( items = history, key = { _, item -> when (item) { - is TrnListItemUi.DateDivider -> item.date + is TrnListItemUi.DateDivider -> item.id is TrnListItemUi.Trn -> item.trn.id is TrnListItemUi.Transfer -> item.batchId } @@ -146,14 +172,15 @@ private fun LazyListScope.history( SpacerVer( // the first date divider require less margin height = if (index > 0 && history[index - 1] !is TrnListItemUi.DateDivider) - 32.dp else 24.dp + 20.dp else 16.dp ) - item.DateDivider() + DateDivider(item) } is TrnListItemUi.Trn -> { SpacerVer(height = 12.dp) - item.trn.Card( + TransactionCard( modifier = Modifier.padding(horizontal = 16.dp), + trn = item.trn, onClick = trnClickHandler.onTrnClick, onAccountClick = trnClickHandler.onAccountClick, onCategoryClick = trnClickHandler.onCategoryClick, @@ -161,9 +188,12 @@ private fun LazyListScope.history( } is TrnListItemUi.Transfer -> { SpacerVer(height = 12.dp) - item.Card( + TransferCard( modifier = Modifier.padding(horizontal = 16.dp), - onClick = trnClickHandler.onTransferClick + transfer = item, + onClick = trnClickHandler.onTransferClick, + onAccountClick = trnClickHandler.onAccountClick, + onCategoryClick = trnClickHandler.onCategoryClick, ) } } diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/DueActions.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/DueActions.kt index 1b70fe2b4a..d35fa6aa2a 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/DueActions.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/DueActions.kt @@ -2,11 +2,14 @@ package com.ivy.core.ui.transaction.card import androidx.compose.runtime.Immutable import com.ivy.core.ui.data.transaction.TransactionUi +import com.ivy.core.ui.data.transaction.TrnListItemUi @Immutable data class DueActions( - val onSkip: (TransactionUi) -> Unit, - val onPayGet: (TransactionUi) -> Unit, + val onSkipTrn: (TransactionUi) -> Unit, + val onExecuteTrn: (TransactionUi) -> Unit, + val onSkipTransfer: (TrnListItemUi.Transfer) -> Unit, + val onExecuteTransfer: (TrnListItemUi.Transfer) -> Unit, ) -fun dummyDueActions() = DueActions({}, {}) \ No newline at end of file +fun dummyDueActions() = DueActions({}, {}, {}, {}) \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/IncomeExpenseCard.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/IncomeExpenseCard.kt index 78b3af2232..a8191a2010 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/IncomeExpenseCard.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/IncomeExpenseCard.kt @@ -9,14 +9,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.ui.R import com.ivy.core.ui.account.AccountBadge import com.ivy.core.ui.category.CategoryBadge -import com.ivy.core.ui.data.AccountUi import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi import com.ivy.core.ui.data.transaction.TransactionUi import com.ivy.core.ui.data.transaction.TrnTimeUi import com.ivy.core.ui.data.transaction.dummyTransactionUi @@ -24,7 +25,6 @@ import com.ivy.core.ui.data.transaction.dummyTrnTimeDueUi import com.ivy.core.ui.value.AmountCurrency import com.ivy.data.transaction.TransactionType import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.Gradient import com.ivy.design.l0_system.color.White import com.ivy.design.l0_system.color.asBrush import com.ivy.design.l1_buildingBlocks.IconRes @@ -33,7 +33,8 @@ import com.ivy.design.l1_buildingBlocks.SpacerVer import com.ivy.design.util.ComponentPreview @Composable -fun TransactionUi.Card( +fun TransactionCard( + trn: TransactionUi, onClick: (TransactionUi) -> Unit, onAccountClick: (AccountUi) -> Unit, onCategoryClick: (CategoryUi) -> Unit, @@ -43,28 +44,31 @@ fun TransactionUi.Card( ) { TransactionCard( modifier = modifier, - onClick = { onClick(this@Card) } + onClick = { onClick(trn) } ) { IncomeExpenseHeader( - account = account, - category = category, + account = trn.account, + category = trn.category, onCategoryClick = onCategoryClick, onAccountClick = onAccountClick ) - DueDate(time = time) - Title(title = title, time = time) - Description(description = description, title = title) - TrnValue(type = type, value = value, time = time) + DueDate(time = trn.time) + Title(title = trn.title, time = trn.time) + Description(description = trn.description, title = trn.title) + TrnValue(type = trn.type, value = trn.value, time = trn.time) if (dueActions != null) { DuePaymentCTAs( - time = time, - type = type, + time = trn.time, + cta = when (trn.type) { + TransactionType.Income -> stringResource(R.string.get) + TransactionType.Expense -> stringResource(R.string.pay) + }, onSkip = { - dueActions.onSkip(this@Card) + dueActions.onSkipTrn(trn) }, - onPayGet = { - dueActions.onPayGet(this@Card) + onExecute = { + dueActions.onExecuteTrn(trn) } ) } @@ -87,7 +91,7 @@ private fun IncomeExpenseHeader( category = it, onClick = { onCategoryClick(category) } ) - SpacerHor(width = 12.dp) + SpacerHor(width = 8.dp) } AccountBadge( @@ -106,7 +110,7 @@ private fun TrnValue( type: TransactionType, time: TrnTimeUi, ) { - SpacerVer(height = 12.dp) + SpacerVer(height = 8.dp) TransactionCardAmountRow { TrnTypeIcon(type = type, time = time) SpacerHor(width = 12.dp) @@ -146,10 +150,7 @@ private fun TrnTypeIcon( StyledIcon( iconId = R.drawable.ic_expense, bgColor = when (time) { - is TrnTimeUi.Actual -> Gradient( - start = UI.colors.red, - end = UI.colors.redP2, - ).asHorizontalBrush() + is TrnTimeUi.Actual -> UI.colors.red.asBrush() is TrnTimeUi.Due -> if (time.upcoming) UI.colors.orange.asBrush() else UI.colors.red.asBrush() }, @@ -173,15 +174,16 @@ private fun TrnTypeIcon( @Composable private fun Preview_Expense() { ComponentPreview { - dummyTransactionUi( - type = TransactionType.Expense, - value = ValueUi( - amount = "0.34", - currency = "BGN" - ), - title = "Order food" - ).Card( + TransactionCard( modifier = Modifier.padding(horizontal = 16.dp), + trn = dummyTransactionUi( + type = TransactionType.Expense, + value = ValueUi( + amount = "0.34", + currency = "BGN" + ), + title = "Order food" + ), onClick = {}, onAccountClick = {}, onCategoryClick = {} @@ -193,15 +195,16 @@ private fun Preview_Expense() { @Composable private fun Preview_Income() { ComponentPreview { - dummyTransactionUi( - type = TransactionType.Income, - value = ValueUi( - amount = "1,005.00", - currency = "USD" - ), - title = "Income" - ).Card( + TransactionCard( modifier = Modifier.padding(horizontal = 16.dp), + trn = dummyTransactionUi( + type = TransactionType.Income, + value = ValueUi( + amount = "1,005.00", + currency = "USD" + ), + title = "Income" + ), onClick = {}, onAccountClick = {}, onCategoryClick = {} @@ -213,17 +216,18 @@ private fun Preview_Income() { @Composable private fun Preview_UpcomingExpense() { ComponentPreview { - dummyTransactionUi( - type = TransactionType.Expense, - value = ValueUi( - amount = "1,005.00", - currency = "USD" - ), - title = "Upcoming Expense", - description = "Description", - time = dummyTrnTimeDueUi(), - ).Card( + TransactionCard( modifier = Modifier.padding(horizontal = 16.dp), + trn = dummyTransactionUi( + type = TransactionType.Expense, + value = ValueUi( + amount = "1,005.00", + currency = "USD" + ), + title = "Upcoming Expense", + description = "Description", + time = dummyTrnTimeDueUi(), + ), onClick = {}, onAccountClick = {}, onCategoryClick = {}, diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransactionCard.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransactionCard.kt index 3463d7159f..60762aaa3c 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransactionCard.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransactionCard.kt @@ -14,15 +14,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.ivy.core.ui.R import com.ivy.core.ui.data.transaction.TrnTimeUi -import com.ivy.data.transaction.TransactionType import com.ivy.design.l0_system.UI -import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.B2 import com.ivy.design.l1_buildingBlocks.CaptionSecond import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.design.l1_buildingBlocks.SpacerVer -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton /** @@ -40,7 +39,7 @@ internal fun TransactionCard( .clip(UI.shapes.rounded) .background(UI.colors.medium, UI.shapes.rounded) .clickable(onClick = onClick) - .padding(all = 20.dp) + .padding(horizontal = 16.dp, vertical = 12.dp) .testTag("transaction_card"), content = content ) @@ -50,9 +49,9 @@ internal fun TransactionCard( @Composable internal fun DueDate(time: TrnTimeUi) { if (time is TrnTimeUi.Due) { - SpacerVer(height = 12.dp) + SpacerVer(height = 8.dp) CaptionSecond( - text = time.dueOn, + text = time.dueOnDate, color = if (time.upcoming) UI.colors.orange else UI.colors.red, fontWeight = FontWeight.Bold ) @@ -67,8 +66,8 @@ internal fun Title( time: TrnTimeUi ) { if (title != null) { - SpacerVer(height = if (time is TrnTimeUi.Due) 8.dp else 8.dp) - B1( + SpacerVer(height = if (time is TrnTimeUi.Due) 4.dp else 8.dp) + B2( text = title, fontWeight = FontWeight.ExtraBold ) @@ -81,7 +80,7 @@ internal fun Description( title: String? ) { if (description != null) { - SpacerVer(height = if (title != null) 4.dp else 8.dp) + SpacerVer(height = if (title != null) 0.dp else 4.dp) CaptionSecond( text = description, color = UI.colors.neutral, @@ -101,8 +100,7 @@ internal fun TransactionCardAmountRow( ) { Row( modifier = modifier - .testTag("type_amount_currency") - .padding(horizontal = 4.dp), // additional padding to look better? + .testTag("type_amount_currency"), verticalAlignment = Alignment.CenterVertically, content = content ) @@ -113,13 +111,12 @@ internal fun TransactionCardAmountRow( @Composable internal fun DuePaymentCTAs( time: TrnTimeUi, - type: TransactionType, + cta: String, onSkip: () -> Unit, - onPayGet: () -> Unit, + onExecute: () -> Unit, ) { if (time is TrnTimeUi.Due) { SpacerVer(height = 12.dp) - Row( modifier = Modifier .fillMaxWidth() @@ -128,7 +125,7 @@ internal fun DuePaymentCTAs( ) { SkipButton(onClick = onSkip) SpacerHor(width = 12.dp) - PayGetButton(type = type, onClick = onPayGet) + ExecutePaymentButton(cta = cta, onClick = onExecute) } } } @@ -140,8 +137,8 @@ private fun RowScope.SkipButton( IvyButton( modifier = Modifier.weight(1f), size = ButtonSize.Big, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Negative, + visibility = Visibility.Medium, + feeling = Feeling.Negative, text = stringResource(R.string.skip), icon = null, onClick = onClick, @@ -149,17 +146,16 @@ private fun RowScope.SkipButton( } @Composable -private fun RowScope.PayGetButton( - type: TransactionType, +private fun RowScope.ExecutePaymentButton( + cta: String, onClick: () -> Unit ) { - val isIncome = type == TransactionType.Income IvyButton( modifier = Modifier.weight(1f), size = ButtonSize.Big, - visibility = ButtonVisibility.High, - feeling = ButtonFeeling.Positive, - text = stringResource(if (isIncome) R.string.get else R.string.pay), + visibility = Visibility.High, + feeling = Feeling.Positive, + text = cta, icon = null, onClick = onClick, ) diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransferCard.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransferCard.kt index 88423a90f6..dc06e239f0 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransferCard.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/card/TransferCard.kt @@ -1,67 +1,113 @@ package com.ivy.core.ui.transaction.card import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.domain.pure.format.dummyValueUi import com.ivy.core.ui.R -import com.ivy.core.ui.data.AccountUi +import com.ivy.core.ui.category.CategoryBadge +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.dummyCategoryUi import com.ivy.core.ui.data.icon.IconSize import com.ivy.core.ui.data.transaction.TrnListItemUi.Transfer import com.ivy.core.ui.data.transaction.dummyTransactionUi import com.ivy.core.ui.data.transaction.dummyTrnTimeActualUi import com.ivy.core.ui.icon.ItemIcon import com.ivy.core.ui.value.AmountCurrency -import com.ivy.data.CurrencyCode import com.ivy.data.transaction.TransactionType import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.rememberContrastColor +import com.ivy.design.l0_system.color.White +import com.ivy.design.l0_system.color.rememberContrast import com.ivy.design.l1_buildingBlocks.* import com.ivy.design.util.ComponentPreview @Composable -fun Transfer.Card( - onClick: (Transfer) -> Unit, +fun TransferCard( + transfer: Transfer, modifier: Modifier = Modifier, + dueActions: DueActions? = null, + onAccountClick: (AccountUi) -> Unit, + onCategoryClick: (CategoryUi) -> Unit, + onClick: (Transfer) -> Unit, ) { TransactionCard( modifier = modifier, - onClick = { onClick(this@Card) } + onClick = { onClick(transfer) } ) { - TransferHeader(account = from.account, toAccount = to.account) - DueDate(time = time) - Title(title = from.title, time = time) - Description(description = from.description, title = from.title) - TransferAmount(fromValue = from.value) - ToAmountDifferentCurrency(fromCurrency = from.value.currency, toValue = to.value) + TransferHeader( + fromAccount = transfer.from.account, + toAccount = transfer.to.account, + onAccountClick = onAccountClick + ) + Category(category = transfer.from.category, onCategoryClick = onCategoryClick) + DueDate(time = transfer.time) + Title(title = transfer.from.title, time = transfer.time) + Description(description = transfer.from.description, title = transfer.from.title) + TransferAmount(fromValue = transfer.from.value) + ToAmountReceived( + fromValue = transfer.from.value, + toValue = transfer.to.value + ) + Fee(fee = transfer.fee?.value) + if (dueActions != null) { + DuePaymentCTAs( + time = transfer.time, + cta = "Execute", + onSkip = { + dueActions.onSkipTransfer(transfer) + }, + onExecute = { + dueActions.onExecuteTransfer(transfer) + }, + ) + } } } @Composable private fun TransferHeader( - account: AccountUi, - toAccount: AccountUi + fromAccount: AccountUi, + toAccount: AccountUi, + onAccountClick: (AccountUi) -> Unit, ) { @Composable - fun AccountUi.IconName() { - ItemIcon( - itemIcon = icon, - size = IconSize.S, - tint = UI.colorsInverted.pure, - ) - SpacerHor(width = 4.dp) - Caption( - text = name, - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) + fun IconAndName( + account: AccountUi, + horizontalArrangement: Arrangement.Horizontal, + modifier: Modifier = Modifier, + onClick: () -> Unit + ) { + Row( + modifier = modifier + .clip(UI.shapes.fullyRounded) + .clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = horizontalArrangement, + ) { + ItemIcon( + itemIcon = account.icon, + size = IconSize.S, + tint = UI.colorsInverted.pure, + ) + SpacerHor(width = 4.dp) + Caption( + text = account.name, + color = UI.colorsInverted.pure, + fontWeight = FontWeight.ExtraBold + ) + } } Row( @@ -71,13 +117,34 @@ private fun TransferHeader( .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { - account.IconName() + IconAndName( + modifier = Modifier.weight(1f), + account = fromAccount, + horizontalArrangement = Arrangement.Start, + onClick = { onAccountClick(fromAccount) } + ) SpacerHor(width = 12.dp) IconRes(R.drawable.ic_arrow_right) SpacerHor(width = 8.dp) - toAccount.IconName() + IconAndName( + modifier = Modifier.weight(1f), + account = toAccount, + horizontalArrangement = Arrangement.End, + onClick = { onAccountClick(toAccount) } + ) + } +} + +@Composable +private fun Category( + category: CategoryUi?, + onCategoryClick: (CategoryUi) -> Unit, +) { + if (category != null) { + SpacerVer(height = 8.dp) + CategoryBadge(category = category, onClick = { onCategoryClick(category) }) } } @@ -85,12 +152,12 @@ private fun TransferHeader( private fun TransferAmount( fromValue: ValueUi, ) { - SpacerVer(height = 12.dp) + SpacerVer(height = 8.dp) TransactionCardAmountRow { IconRes( modifier = Modifier.background(UI.colors.primary, UI.shapes.circle), icon = R.drawable.ic_transfer, - tint = rememberContrastColor(UI.colors.primary), + tint = rememberContrast(UI.colors.primary), ) SpacerHor(width = 12.dp) AmountCurrency(value = fromValue, color = UI.colors.primary) @@ -98,41 +165,69 @@ private fun TransferAmount( } @Composable -private fun ToAmountDifferentCurrency( - fromCurrency: CurrencyCode, +private fun ToAmountReceived( + fromValue: ValueUi, toValue: ValueUi, ) { - if (fromCurrency != toValue.currency) { + if (fromValue != toValue) { B2Second( text = "${toValue.amount} ${toValue.currency}", - modifier = Modifier.padding(start = 48.dp), + modifier = Modifier.padding(start = 44.dp), color = UI.colors.neutral, fontWeight = FontWeight.Normal, ) } } +@Composable +private fun Fee( + fee: ValueUi?, +) { + if (fee != null) { + SpacerVer(height = 8.dp) + TransactionCardAmountRow { + IconRes( + modifier = Modifier.background(UI.colors.red, UI.shapes.circle), + icon = R.drawable.ic_expense, + tint = White + ) + SpacerHor(width = 12.dp) + AmountCurrency(value = fee, color = UI.colors.red) + SpacerHor(width = 4.dp) + B2Second( + text = "(FEE)", + color = UI.colors.red, + fontWeight = FontWeight.Normal + ) + } + } +} + // region Previews @Preview @Composable private fun Preview_SameCurrency() { ComponentPreview { - Transfer( - batchId = "", - time = dummyTrnTimeActualUi(), - from = dummyTransactionUi( - type = TransactionType.Expense, - value = dummyValueUi(amount = "400") - ), - to = dummyTransactionUi( - type = TransactionType.Expense, - value = dummyValueUi(amount = "400") - ), - fee = null - ).Card( + TransferCard( modifier = Modifier.padding(horizontal = 16.dp), - onClick = {} + transfer = Transfer( + batchId = "", + time = dummyTrnTimeActualUi(), + from = dummyTransactionUi( + type = TransactionType.Expense, + value = dummyValueUi(amount = "400") + ), + to = dummyTransactionUi( + type = TransactionType.Income, + value = dummyValueUi(amount = "400") + ), + fee = null + ), + onAccountClick = {}, + onCategoryClick = {}, + onClick = {}, + dueActions = dummyDueActions(), ) } } @@ -141,23 +236,60 @@ private fun Preview_SameCurrency() { @Composable private fun Preview_Detailed() { ComponentPreview { - Transfer( - batchId = "", - time = dummyTrnTimeActualUi(), - from = dummyTransactionUi( - title = "Withdrawing cash", - description = "So I can pay rent", - type = TransactionType.Expense, - value = dummyValueUi(amount = "400", currency = "EUR") - ), - to = dummyTransactionUi( - type = TransactionType.Expense, - value = dummyValueUi(amount = "800", currency = "BGN") + TransferCard( + modifier = Modifier.padding(horizontal = 16.dp), + transfer = Transfer( + batchId = "", + time = dummyTrnTimeActualUi(), + from = dummyTransactionUi( + title = "Withdrawing cash", + description = "So I can pay rent", + category = dummyCategoryUi(), + type = TransactionType.Expense, + value = dummyValueUi(amount = "400", currency = "EUR") + ), + to = dummyTransactionUi( + type = TransactionType.Income, + value = dummyValueUi(amount = "800", currency = "BGN") + ), + fee = dummyTransactionUi( + type = TransactionType.Expense, + value = dummyValueUi("2") + ) ), - fee = null - ).Card( + onAccountClick = {}, + onCategoryClick = {}, + onClick = {}, + dueActions = dummyDueActions(), + ) + } +} + +@Preview +@Composable +private fun Preview_LongAccount_names() { + ComponentPreview { + TransferCard( modifier = Modifier.padding(horizontal = 16.dp), - onClick = {} + transfer = Transfer( + batchId = "", + time = dummyTrnTimeActualUi(), + from = dummyTransactionUi( + type = TransactionType.Expense, + account = dummyAccountUi(name = "My very long account name"), + value = dummyValueUi(amount = "400", currency = "EUR") + ), + to = dummyTransactionUi( + type = TransactionType.Income, + account = dummyAccountUi(name = "Revolut Business Company Account"), + value = dummyValueUi(amount = "800", currency = "BGN") + ), + fee = null + ), + onAccountClick = {}, + onCategoryClick = {}, + onClick = {}, + dueActions = dummyDueActions(), ) } } diff --git a/core/ui/src/main/java/com/ivy/core/ui/transaction/handling/TrnItemClickHandler.kt b/core/ui/src/main/java/com/ivy/core/ui/transaction/handling/TrnItemClickHandler.kt index baae731850..8f81e054b9 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/transaction/handling/TrnItemClickHandler.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/transaction/handling/TrnItemClickHandler.kt @@ -3,11 +3,11 @@ package com.ivy.core.ui.transaction.handling import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import com.ivy.core.domain.HandlerViewModel -import com.ivy.core.ui.data.AccountUi import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi import com.ivy.core.ui.data.transaction.TransactionUi import com.ivy.core.ui.data.transaction.TrnListItemUi -import com.ivy.design.util.hiltViewmodelPreviewSafe +import com.ivy.design.util.hiltViewModelPreviewSafe import com.ivy.navigation.Navigator import com.ivy.navigation.destinations.Destination import dagger.hilt.android.lifecycle.HiltViewModel @@ -58,7 +58,7 @@ class TrnItemClickHandlerViewModel @Inject constructor( @Composable fun defaultTrnItemClickHandler(): TrnItemClickHandler { - val viewModel: TrnItemClickHandlerViewModel? = hiltViewmodelPreviewSafe() + val viewModel: TrnItemClickHandlerViewModel? = hiltViewModelPreviewSafe() return TrnItemClickHandler( onAccountClick = { diff --git a/core/ui/src/main/java/com/ivy/core/ui/value/AmountCurrency.kt b/core/ui/src/main/java/com/ivy/core/ui/value/AmountCurrency.kt index 8b55f8ba13..0c2e260242 100644 --- a/core/ui/src/main/java/com/ivy/core/ui/value/AmountCurrency.kt +++ b/core/ui/src/main/java/com/ivy/core/ui/value/AmountCurrency.kt @@ -1,5 +1,6 @@ package com.ivy.core.ui.value +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.RowScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -10,8 +11,49 @@ import androidx.compose.ui.unit.dp import com.ivy.core.domain.pure.format.ValueUi import com.ivy.design.l0_system.UI import com.ivy.design.l1_buildingBlocks.B1Second +import com.ivy.design.l1_buildingBlocks.B2Second +import com.ivy.design.l1_buildingBlocks.H2Second import com.ivy.design.l1_buildingBlocks.SpacerHor +@Suppress("unused") +@Composable +fun ColumnScope.AmountCurrencyBig( + value: ValueUi, + color: Color = UI.colorsInverted.pure, +) { + H2Second( + text = value.amount, + modifier = Modifier.testTag("amount_currency_b1"), + fontWeight = FontWeight.Bold, + color = color, + ) + B1Second( + text = value.currency, + fontWeight = FontWeight.Normal, + color = color, + ) +} + +@Suppress("unused") +@Composable +fun RowScope.AmountCurrencyBig( + value: ValueUi, + color: Color = UI.colorsInverted.pure, +) { + H2Second( + text = value.amount, + modifier = Modifier.testTag("amount_currency_b1"), + fontWeight = FontWeight.Bold, + color = color, + ) + SpacerHor(width = 4.dp) + B1Second( + text = value.currency, + fontWeight = FontWeight.SemiBold, + color = color, + ) +} + @Suppress("unused") @Composable fun RowScope.AmountCurrency( @@ -30,4 +72,24 @@ fun RowScope.AmountCurrency( fontWeight = FontWeight.Normal, color = color, ) +} + +@Suppress("unused") +@Composable +fun RowScope.AmountCurrencySmall( + value: ValueUi, + color: Color = UI.colorsInverted.pure, +) { + B2Second( + text = value.amount, + modifier = Modifier.testTag("amount_currency_b1"), + fontWeight = FontWeight.Bold, + color = color, + ) + SpacerHor(width = 4.dp) + B2Second( + text = value.currency, + fontWeight = FontWeight.Normal, + color = color, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/ivy/core/ui/value/ValueFunctions.kt b/core/ui/src/main/java/com/ivy/core/ui/value/ValueFunctions.kt deleted file mode 100644 index 10d846f513..0000000000 --- a/core/ui/src/main/java/com/ivy/core/ui/value/ValueFunctions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.ivy.core.ui.value - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import com.ivy.data.Value -import com.ivy.wallet.utils.format -import com.ivy.wallet.utils.shortenAmount -import com.ivy.wallet.utils.shouldShortAmount - - -@Deprecated("don't use!") -@Composable -fun Value.formatAmount( - shortenBigNumbers: Boolean -) = remember(amount, shortenBigNumbers, currency) { - val shortAmount = shortenBigNumbers && shouldShortAmount(amount) - if (shortAmount) shortenAmount(amount) else amount.format(currency) -} \ No newline at end of file diff --git a/debug/src/main/java/com/ivy/debug/TestEvent.kt b/debug/src/main/java/com/ivy/debug/TestEvent.kt new file mode 100644 index 0000000000..179df81caa --- /dev/null +++ b/debug/src/main/java/com/ivy/debug/TestEvent.kt @@ -0,0 +1,7 @@ +package com.ivy.debug + +import com.ivy.data.CurrencyCode + +sealed interface TestEvent { + data class BaseCurrencyChange(val currency: CurrencyCode) : TestEvent +} \ No newline at end of file diff --git a/debug/src/main/java/com/ivy/debug/TestScreen.kt b/debug/src/main/java/com/ivy/debug/TestScreen.kt index a7257f9b0a..0f90cd03f7 100644 --- a/debug/src/main/java/com/ivy/debug/TestScreen.kt +++ b/debug/src/main/java/com/ivy/debug/TestScreen.kt @@ -7,10 +7,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.ivy.core.ui.amount.AmountModal import com.ivy.core.ui.color.ColorPickerButton import com.ivy.core.ui.color.picker.ColorPickerModal +import com.ivy.core.ui.currency.CurrencyPickerModal import com.ivy.core.ui.icon.picker.IconPickerModal import com.ivy.data.ItemIconId +import com.ivy.data.Value import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.color.Purple import com.ivy.design.l1_buildingBlocks.ColumnRoot @@ -18,16 +21,20 @@ import com.ivy.design.l1_buildingBlocks.H2 import com.ivy.design.l1_buildingBlocks.SpacerVer import com.ivy.design.l1_buildingBlocks.SpacerWeight import com.ivy.design.l2_components.modal.rememberIvyModal -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton @Composable fun BoxScope.TestScreen() { val viewModel: TestViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + val iconPickerModal = rememberIvyModal() val colorPickerModal = rememberIvyModal() + val amountModal = rememberIvyModal() + val currencyPickerModal = rememberIvyModal() var selectedIconId by remember { mutableStateOf(null) } var selectedColor by remember { mutableStateOf(Purple) } @@ -39,8 +46,8 @@ fun BoxScope.TestScreen() { SpacerWeight(weight = 1f) IvyButton( size = ButtonSize.Big, - visibility = ButtonVisibility.Focused, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Focused, + feeling = Feeling.Positive, text = "Pick an icon", icon = null ) { @@ -53,6 +60,26 @@ fun BoxScope.TestScreen() { colorPickerModal = colorPickerModal, selectedColor = selectedColor ) + SpacerVer(height = 48.dp) + IvyButton( + size = ButtonSize.Big, + visibility = Visibility.Focused, + feeling = Feeling.Positive, + text = "Amount modal", + icon = null + ) { + amountModal.show() + } + SpacerVer(height = 48.dp) + IvyButton( + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "Base currency: ${state.baseCurrency}", + icon = null + ) { + currencyPickerModal.show() + } SpacerWeight(weight = 1f) } @@ -62,10 +89,21 @@ fun BoxScope.TestScreen() { color = UI.colors.primary, onIconPick = { selectedIconId = it } ) - ColorPickerModal( modal = colorPickerModal, initialColor = selectedColor, onColorPicked = { selectedColor = it } ) + AmountModal( + modal = amountModal, + initialAmount = Value(0.0, "USD"), + onAmountEnter = {} + ) + CurrencyPickerModal( + modal = currencyPickerModal, + initialCurrency = state.baseCurrency, + onCurrencyPick = { + viewModel.onEvent(TestEvent.BaseCurrencyChange(it)) + } + ) } \ No newline at end of file diff --git a/debug/src/main/java/com/ivy/debug/TestStateUi.kt b/debug/src/main/java/com/ivy/debug/TestStateUi.kt index a877e1b121..7cd86cc63b 100644 --- a/debug/src/main/java/com/ivy/debug/TestStateUi.kt +++ b/debug/src/main/java/com/ivy/debug/TestStateUi.kt @@ -1,7 +1,9 @@ package com.ivy.debug import com.ivy.core.ui.data.period.SelectedPeriodUi +import com.ivy.data.CurrencyCode data class TestStateUi( - val selectedPeriodUi: SelectedPeriodUi + val selectedPeriodUi: SelectedPeriodUi, + val baseCurrency: CurrencyCode, ) \ No newline at end of file diff --git a/debug/src/main/java/com/ivy/debug/TestViewModel.kt b/debug/src/main/java/com/ivy/debug/TestViewModel.kt index 8ad70e529e..229f6495d8 100644 --- a/debug/src/main/java/com/ivy/debug/TestViewModel.kt +++ b/debug/src/main/java/com/ivy/debug/TestViewModel.kt @@ -1,37 +1,49 @@ package com.ivy.debug -import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.SimpleFlowViewModel import com.ivy.core.domain.action.period.SelectedPeriodFlow +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow +import com.ivy.core.domain.action.settings.basecurrency.WriteBaseCurrencyAct import com.ivy.core.domain.pure.time.allTime import com.ivy.core.ui.action.mapping.MapSelectedPeriodUiAct import com.ivy.core.ui.data.period.SelectedPeriodUi import com.ivy.core.ui.data.period.TimeRangeUi import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import javax.inject.Inject @HiltViewModel class TestViewModel @Inject constructor( - private val selectedPeriodFlow: SelectedPeriodFlow, - private val mapSelectedPeriodAct: MapSelectedPeriodUiAct -) : FlowViewModel() { - override fun initialState() = TestStateUi( + selectedPeriodFlow: SelectedPeriodFlow, + private val mapSelectedPeriodAct: MapSelectedPeriodUiAct, + private val writeBaseCurrencyAct: WriteBaseCurrencyAct, + baseCurrencyFlow: BaseCurrencyFlow, +) : SimpleFlowViewModel() { + override val initialUi: TestStateUi = TestStateUi( selectedPeriodUi = SelectedPeriodUi.AllTime( btnText = "", rangeUi = TimeRangeUi(allTime(), "", "") - ) + ), + baseCurrency = "" ) - override fun initialUiState(): TestStateUi = initialState() - - override fun stateFlow(): Flow = selectedPeriodFlow().map { + override val uiFlow: Flow = combine( + selectedPeriodFlow(), baseCurrencyFlow() + ) { period, baseCurrency -> TestStateUi( - selectedPeriodUi = mapSelectedPeriodAct(it) + selectedPeriodUi = mapSelectedPeriodAct(period), + baseCurrency = baseCurrency, ) } - override suspend fun mapToUiState(state: TestStateUi): TestStateUi = state + // region Event handling + override suspend fun handleEvent(event: TestEvent) = when (event) { + is TestEvent.BaseCurrencyChange -> handleBaseCurrencyChange(event) + } - override suspend fun handleEvent(event: Unit) {} + private suspend fun handleBaseCurrencyChange(event: TestEvent.BaseCurrencyChange) { + writeBaseCurrencyAct(event.currency) + } + // endregion } \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/IvyContext.kt b/design-system/src/main/java/com/ivy/design/IvyContext.kt index a0f7363157..21aa41f4f4 100644 --- a/design-system/src/main/java/com/ivy/design/IvyContext.kt +++ b/design-system/src/main/java/com/ivy/design/IvyContext.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.setValue @Deprecated("bad idea - don't use it") abstract class IvyContext { - var theme: com.ivy.data.Theme by mutableStateOf(com.ivy.data.Theme.LIGHT) + var theme: com.ivy.data.ThemeOld by mutableStateOf(com.ivy.data.ThemeOld.LIGHT) private set var screenWidth: Int = -1 @@ -18,7 +18,7 @@ abstract class IvyContext { return if (field > 0) field else throw IllegalStateException("screenHeight not initialized") } - fun switchTheme(theme: com.ivy.data.Theme) { + fun switchTheme(theme: com.ivy.data.ThemeOld) { this.theme = theme } } \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/Theme.kt b/design-system/src/main/java/com/ivy/design/Theme.kt deleted file mode 100644 index caa644a4de..0000000000 --- a/design-system/src/main/java/com/ivy/design/Theme.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.design - -enum class Theme { - Light, Dark, Auto -} \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/animation/AnimationUtil.kt b/design-system/src/main/java/com/ivy/design/animation/AnimationUtil.kt new file mode 100644 index 0000000000..cdb4701294 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/animation/AnimationUtil.kt @@ -0,0 +1,8 @@ +package com.ivy.design.animation + +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically + +fun slideInBottom() = slideInVertically { it } + +fun slideOutBottom() = slideOutVertically { it } \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/api/IvyUI.kt b/design-system/src/main/java/com/ivy/design/api/IvyUI.kt index f81d635311..adc3b70fc0 100644 --- a/design-system/src/main/java/com/ivy/design/api/IvyUI.kt +++ b/design-system/src/main/java/com/ivy/design/api/IvyUI.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.ivy.design.Theme +import com.ivy.data.Theme import com.ivy.design.api.systems.ivyWalletDesign import com.ivy.design.l0_system.IvyTheme import com.ivy.design.l0_system.UI diff --git a/design-system/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt b/design-system/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt index 9a9e020f18..38efffcc6e 100644 --- a/design-system/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt +++ b/design-system/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt @@ -8,8 +8,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.ivy.data.Theme import com.ivy.design.R -import com.ivy.design.Theme import com.ivy.design.api.IvyDesign import com.ivy.design.l0_system.IvyShapes import com.ivy.design.l0_system.IvyTypography diff --git a/design-system/src/main/java/com/ivy/design/l0_system/color/ColorUtil.kt b/design-system/src/main/java/com/ivy/design/l0_system/color/ColorUtil.kt index 40e13612b6..1949a94119 100644 --- a/design-system/src/main/java/com/ivy/design/l0_system/color/ColorUtil.kt +++ b/design-system/src/main/java/com/ivy/design/l0_system/color/ColorUtil.kt @@ -11,7 +11,7 @@ import androidx.core.graphics.ColorUtils // region Contrast color @Composable -fun rememberContrastColor(color: Color): Color = remember(color) { +fun rememberContrast(color: Color): Color = remember(color) { contrastColor(color) } diff --git a/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/HapticFeedback.kt b/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/HapticFeedback.kt new file mode 100644 index 0000000000..c691bc7a51 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/HapticFeedback.kt @@ -0,0 +1,44 @@ +package com.ivy.design.l1_buildingBlocks + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback + +@OptIn(ExperimentalFoundationApi::class) +fun Modifier.hapticClickable( + enabled: Boolean = true, + hapticFeedbackEnabled: Boolean = true, + onLongClick: (() -> Unit)? = null, + onClick: () -> Unit +): Modifier { + return if (hapticFeedbackEnabled) composed { + // with haptic feedback + val hapticFeedback = LocalHapticFeedback.current + + if (onLongClick != null) this.combinedClickable( + enabled = enabled, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onClick() + }, + onLongClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() + } + ) else this.clickable(enabled = enabled) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onClick() + } + } else { + // no haptic feedback + if (onLongClick != null) this.combinedClickable( + enabled = enabled, + onClick = onClick, + onLongClick = onLongClick, + ) else this.clickable(enabled = enabled, onClick = onClick) + } +} \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/InputField.kt b/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/InputField.kt index 9845460df4..77ff45cd7c 100644 --- a/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/InputField.kt +++ b/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/InputField.kt @@ -1,5 +1,6 @@ package com.ivy.design.l1_buildingBlocks +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActionScope @@ -9,6 +10,7 @@ import androidx.compose.material.OutlinedTextField import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle @@ -19,6 +21,8 @@ import androidx.compose.ui.unit.dp import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.design.util.ComponentPreview +import com.ivy.resources.R +import kotlinx.coroutines.delay @Composable fun InputField( @@ -30,7 +34,10 @@ fun InputField( enabled: Boolean = true, readOnly: Boolean = false, isError: Boolean = false, + @DrawableRes + iconLeft: Int? = null, shape: Shape = UI.shapes.rounded, + focusedColor: Color = UI.colors.primary, textStyle: TextStyle = UI.typo.b2.style(fontWeight = FontWeight.Bold), keyboardType: KeyboardType = KeyboardType.Text, keyboardCapitalization: KeyboardCapitalization = KeyboardCapitalization.None, @@ -46,15 +53,34 @@ fun InputField( val selection = TextRange(initialValue.length) mutableStateOf(TextFieldValue(initialValue, selection)) } + LaunchedEffect(initialValue) { + if (initialValue != textField.text && initialValue.isNotBlank()) { + delay(50) // fix race condition + textField = TextFieldValue( + initialValue, TextRange(initialValue.length) + ) + } + } OutlinedTextField( modifier = modifier, value = textField, - onValueChange = { - textField = it - onValueChange(it.text) + onValueChange = { newValue -> + // new value different than the current one + if (newValue.text != textField.text) { + onValueChange(newValue.text) + } + textField = newValue }, shape = shape, textStyle = textStyle, + leadingIcon = if (iconLeft != null) { + { + IconRes( + modifier = Modifier.padding(start = 4.dp), + icon = iconLeft, + ) + } + } else null, placeholder = { Text( text = placeholder, @@ -71,8 +97,8 @@ fun InputField( textColor = UI.colorsInverted.pure, cursorColor = UI.colorsInverted.pure, backgroundColor = UI.colors.pure, - focusedBorderColor = UI.colors.primary, - focusedLabelColor = UI.colors.primary, + focusedBorderColor = focusedColor, + focusedLabelColor = focusedColor, disabledBorderColor = UI.colors.neutral, disabledLabelColor = UI.colors.neutral, errorBorderColor = UI.colors.red, @@ -119,6 +145,7 @@ private fun Preview_Hint() { .fillMaxWidth() .padding(horizontal = 16.dp), initialValue = "", + iconLeft = R.drawable.round_search_24, placeholder = "Placeholder", singleLine = true, maxLines = 1, @@ -135,6 +162,7 @@ private fun Preview_Text() { modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), + iconLeft = R.drawable.round_search_24, initialValue = "Input", placeholder = "Placeholder", singleLine = true, diff --git a/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/data/Background.kt b/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/data/Background.kt index 7cbedf1afa..7c23bdbbf8 100644 --- a/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/data/Background.kt +++ b/design-system/src/main/java/com/ivy/design/l1_buildingBlocks/data/Background.kt @@ -87,6 +87,8 @@ fun solidWithBorder( borderWidth = borderWidth, padding = padding ) + +fun none(): Background.None = Background.None // endregion @Immutable diff --git a/design-system/src/main/java/com/ivy/design/l2_components/Switch.kt b/design-system/src/main/java/com/ivy/design/l2_components/Switch.kt index dd6be4155d..41b6de4fb9 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/Switch.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/Switch.kt @@ -25,7 +25,7 @@ import com.ivy.design.util.springBounce fun Switch( enabled: Boolean, modifier: Modifier = Modifier, - enabledColor: Color = UI.colors.green, + enabledColor: Color = UI.colors.primary, disabledColor: Color = UI.colors.neutral, animationColor: AnimationSpec = springBounce(), animationMove: AnimationSpec = springBounce(), @@ -38,7 +38,7 @@ fun Switch( Row( modifier = modifier - .width(40.dp) + .width(48.dp) .clip(UI.shapes.fullyRounded) .border(2.dp, color, UI.shapes.fullyRounded) .clickable { @@ -74,16 +74,24 @@ fun Switch( } } +// region Preview @Preview @Composable -private fun PreviewIvySwitch() { +private fun Preview_Disabled() { ComponentPreview { - var enabled by remember { - mutableStateOf(false) - } + var enabled by remember { mutableStateOf(false) } - Switch(enabled = enabled) { - enabled = it - } + Switch(enabled = enabled) { enabled = it } + } +} + +@Preview +@Composable +private fun Preview_Enabled() { + ComponentPreview { + var enabled by remember { mutableStateOf(true) } + + Switch(enabled = enabled) { enabled = it } } -} \ No newline at end of file +} +// endregion \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l2_components/SwitchRow.kt b/design-system/src/main/java/com/ivy/design/l2_components/SwitchRow.kt new file mode 100644 index 0000000000..f1f7f7e1e2 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l2_components/SwitchRow.kt @@ -0,0 +1,59 @@ +package com.ivy.design.l2_components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.util.ComponentPreview + +@Composable +fun SwitchRow( + enabled: Boolean, + text: String, + modifier: Modifier = Modifier, + onValueChange: (Boolean) -> Unit +) { + Row( + modifier = modifier + .clip(UI.shapes.rounded) + .defaultMinSize(minHeight = 48.dp) + .clickable { + onValueChange(!enabled) + } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + B2( + modifier = Modifier + .weight(1f) + .padding(end = 12.dp), + text = text + ) + Switch(enabled = enabled, onEnabledChange = onValueChange) + } +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + SwitchRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + enabled = true, + text = "Switch Row", + onValueChange = {} + ) + } +} \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l2_components/button/Button.kt b/design-system/src/main/java/com/ivy/design/l2_components/button/Button.kt index 8ab669e6bb..e7a7c51311 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/button/Button.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/button/Button.kt @@ -1,6 +1,5 @@ package com.ivy.design.l2_components.button -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Text @@ -15,6 +14,7 @@ import com.ivy.design.l0_system.color.White import com.ivy.design.l0_system.colorAs import com.ivy.design.l0_system.style import com.ivy.design.l1_buildingBlocks.data.* +import com.ivy.design.l1_buildingBlocks.hapticClickable import com.ivy.design.util.ComponentPreview import com.ivy.design.util.padding @@ -35,12 +35,13 @@ fun Btn.Text( color = White, textAlign = TextAlign.Center ), + hapticFeedback: Boolean = false, onClick: () -> Unit ) { Text( modifier = modifier .clipBackground(background) - .clickable(onClick = onClick) + .hapticClickable(hapticFeedbackEnabled = hapticFeedback, onClick = onClick) .applyBackground(background), text = text, style = textStyle diff --git a/design-system/src/main/java/com/ivy/design/l2_components/button/ButtonTextIcon.kt b/design-system/src/main/java/com/ivy/design/l2_components/button/ButtonTextIcon.kt index 6ce1a46a2b..7645940fd5 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/button/ButtonTextIcon.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/button/ButtonTextIcon.kt @@ -1,7 +1,6 @@ package com.ivy.design.l2_components.button import androidx.annotation.DrawableRes -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -27,6 +26,7 @@ import com.ivy.design.l1_buildingBlocks.data.Background import com.ivy.design.l1_buildingBlocks.data.applyBackground import com.ivy.design.l1_buildingBlocks.data.clipBackground import com.ivy.design.l1_buildingBlocks.data.solid +import com.ivy.design.l1_buildingBlocks.hapticClickable import com.ivy.design.util.ComponentPreview import com.ivy.design.util.padding @@ -52,12 +52,13 @@ fun Btn.TextIcon( @DrawableRes iconRight: Int? = null, iconTint: Color = White, iconPadding: Dp = 12.dp, + hapticFeedback: Boolean = false, onClick: () -> Unit ) { Row( modifier = modifier .clipBackground(background) - .clickable(onClick = onClick) + .hapticClickable(hapticFeedbackEnabled = hapticFeedback, onClick = onClick) .applyBackground(background), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween diff --git a/design-system/src/main/java/com/ivy/design/l2_components/button/IconButton.kt b/design-system/src/main/java/com/ivy/design/l2_components/button/IconButton.kt index 2097f6efcc..04595208ed 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/button/IconButton.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/button/IconButton.kt @@ -1,7 +1,6 @@ package com.ivy.design.l2_components.button import androidx.annotation.DrawableRes -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable @@ -17,6 +16,7 @@ import com.ivy.design.l1_buildingBlocks.data.Background import com.ivy.design.l1_buildingBlocks.data.applyBackground import com.ivy.design.l1_buildingBlocks.data.clipBackground import com.ivy.design.l1_buildingBlocks.data.solid +import com.ivy.design.l1_buildingBlocks.hapticClickable import com.ivy.design.util.ComponentPreview import com.ivy.design.util.padding @@ -32,12 +32,13 @@ fun Btn.Icon( padding = padding(all = 8.dp) ), contentDescription: String = "icon button", + hapticFeedback: Boolean = false, onClick: () -> Unit ) { IconRes( modifier = modifier .clipBackground(background) - .clickable(onClick = onClick) + .hapticClickable(hapticFeedbackEnabled = hapticFeedback, onClick = onClick) .applyBackground(background), icon = icon, tint = iconTint, diff --git a/design-system/src/main/java/com/ivy/design/l2_components/input/InputFieldType.kt b/design-system/src/main/java/com/ivy/design/l2_components/input/InputFieldType.kt index c3c2a5b095..b28f8342d7 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/input/InputFieldType.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/input/InputFieldType.kt @@ -2,5 +2,5 @@ package com.ivy.design.l2_components.input sealed interface InputFieldType { object SingleLine : InputFieldType - data class Multiline(val maxLines: Int) : InputFieldType + data class Multiline(val maxLines: Int = Int.MAX_VALUE) : InputFieldType } \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l2_components/input/IvyInputField.kt b/design-system/src/main/java/com/ivy/design/l2_components/input/IvyInputField.kt index 8fcce9d8e7..69c3dd0700 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/input/IvyInputField.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/input/IvyInputField.kt @@ -1,10 +1,14 @@ package com.ivy.design.l2_components.input +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActionScope import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -14,8 +18,11 @@ import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.design.l1_buildingBlocks.InputField import com.ivy.design.l2_components.input.InputFieldType.Multiline +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.button.toColor import com.ivy.design.util.ComponentPreview +@OptIn(ExperimentalComposeUiApi::class) @Composable fun IvyInputField( type: InputFieldType, @@ -23,19 +30,25 @@ fun IvyInputField( placeholder: String, modifier: Modifier = Modifier, isError: Boolean = false, + @DrawableRes + iconLeft: Int? = null, + shape: Shape = UI.shapes.rounded, + feeling: Feeling = Feeling.Positive, typography: InputFieldTypography = InputFieldTypography.Primary, keyboardCapitalization: KeyboardCapitalization = KeyboardCapitalization.None, - imeAction: ImeAction = ImeAction.Done, - onImeAction: KeyboardActionScope.(ImeAction) -> Unit = { - defaultKeyboardAction(it) - }, + imeAction: ImeAction = if (type is Multiline) ImeAction.Default else ImeAction.Done, + onImeAction: (KeyboardActionScope.(ImeAction) -> Unit)? = null, onValueChange: (String) -> Unit, ) { + val keyboardController = LocalSoftwareKeyboardController.current InputField( modifier = modifier, initialValue = initialValue, placeholder = placeholder, isError = isError, + iconLeft = iconLeft, + shape = shape, + focusedColor = feeling.toColor(), textStyle = when (typography) { InputFieldTypography.Primary -> UI.typo.b2.style(fontWeight = FontWeight.Bold) InputFieldTypography.Secondary -> UI.typoSecond.b2.style(fontWeight = FontWeight.Bold) @@ -50,7 +63,9 @@ fun IvyInputField( }, keyboardCapitalization = keyboardCapitalization, imeAction = imeAction, - onImeAction = onImeAction, + onImeAction = { + onImeAction?.invoke(this, it) ?: keyboardController?.hide() + }, onValueChange = onValueChange ) } diff --git a/design-system/src/main/java/com/ivy/design/l2_components/modal/Modal.kt b/design-system/src/main/java/com/ivy/design/l2_components/modal/Modal.kt index 7c5aa2d6b8..258e3be78e 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/modal/Modal.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/modal/Modal.kt @@ -8,20 +8,17 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.doOnLayout import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.color.mediumBlur import com.ivy.design.l1_buildingBlocks.SpacerHor @@ -36,13 +33,15 @@ import com.ivy.design.l2_components.modal.scope.ModalActionsScope import com.ivy.design.l2_components.modal.scope.ModalActionsScopeImpl import com.ivy.design.l2_components.modal.scope.ModalScope import com.ivy.design.l2_components.modal.scope.ModalScopeImpl -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.* import com.ivy.resources.R +var openModals = 0 + // region Ivy Modal @Immutable data class IvyModal( @@ -50,10 +49,12 @@ data class IvyModal( ) { fun hide() { visibilityState.value = false + openModals-- } fun show() { visibilityState.value = true + openModals++ } } @@ -61,11 +62,12 @@ data class IvyModal( fun rememberIvyModal(): IvyModal = remember { IvyModal() } // endregion +@OptIn(ExperimentalComposeUiApi::class) @Composable fun BoxScope.Modal( modal: IvyModal, - actions: @Composable ModalActionsScope.() -> Unit, + contentModifier: Modifier = Modifier, keyboardShiftsContent: Boolean = true, level: Int = 1, content: @Composable ModalScope.() -> Unit @@ -80,6 +82,7 @@ fun BoxScope.Modal( enter = fadeIn(), exit = fadeOut() ) { + val keyboardController = LocalSoftwareKeyboardController.current Spacer( modifier = Modifier .fillMaxSize() @@ -87,6 +90,7 @@ fun BoxScope.Modal( .testTag("modal_outside_blur") .clickable( onClick = { + keyboardController?.hide() modal.hide() }, enabled = visible @@ -108,7 +112,7 @@ fun BoxScope.Modal( ) { val systemBottomPadding = systemPaddingBottom() val keyboardShown by keyboardShownState() - val keyboardShownInset = keyboardPaddingBottom() + val keyboardShownInset = keyboardPadding() val paddingBottom = if (keyboardShiftsContent) { animateDpAsState( targetValue = if (keyboardShown) @@ -117,7 +121,7 @@ fun BoxScope.Modal( } else systemBottomPadding Column( - modifier = Modifier + modifier = contentModifier .fillMaxWidth() .statusBarsPadding() .padding(top = 24.dp) // 24 dp from the status bar (top) @@ -135,9 +139,13 @@ fun BoxScope.Modal( content() } + val keyboardController = LocalSoftwareKeyboardController.current ModalActionsRow( Actions = actions, - onClose = { modal.hide() }, + onClose = { + keyboardController?.hide() + modal.hide() + }, ) SpacerVer(height = 12.dp) } @@ -201,79 +209,16 @@ fun CloseButton( onClick: () -> Unit ) { IvyButton( + modifier = modifier, size = ButtonSize.Small, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Disabled, + visibility = Visibility.Medium, + feeling = Feeling.Disabled, text = null, icon = R.drawable.ic_round_close_24, onClick = onClick ) } -@Composable -private fun keyboardShownState(): MutableState { - val keyboardOpen = remember { mutableStateOf(false) } - val rootView = LocalView.current - - DisposableEffect(Unit) { - val keyboardListener = { - // check keyboard state after this layout - val isOpenNew = isKeyboardOpen(rootView) - - // since the observer is hit quite often, only callback when there is a change. - if (isOpenNew != keyboardOpen.value) { - keyboardOpen.value = isOpenNew - } - } - - rootView.doOnLayout { - // get initial state of keyboard - keyboardOpen.value = isKeyboardOpen(rootView) - - // whenever the layout resizes/changes, callback with the state of the keyboard. - rootView.viewTreeObserver.addOnGlobalLayoutListener(keyboardListener) - } - - onDispose { - // stop keyboard updates - rootView.viewTreeObserver.removeOnGlobalLayoutListener(keyboardListener) - } - } - - return keyboardOpen -} - -// region Insets -/** - * @return system's bottom inset (nav buttons or bottom nav) - */ -@Composable -private fun systemPaddingBottom(): Dp { - val rootView = LocalView.current - val densityScope = LocalDensity.current - return remember(rootView) { - val insetPx = - WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets, rootView) - .getInsets(WindowInsetsCompat.Type.navigationBars()) - .bottom - with(densityScope) { insetPx.toDp() } - } -} - -@Composable -private fun keyboardPaddingBottom(): Dp { - val rootView = LocalView.current - val insetPx = - WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets, rootView) - .getInsets( - WindowInsetsCompat.Type.ime() or - WindowInsetsCompat.Type.navigationBars() - ) - .bottom - return insetPx.toDensityDp() -} -// endregion - // region Previews @Preview @Composable @@ -324,8 +269,8 @@ private fun Preview_Partial() { actions = { IvyButton( size = ButtonSize.Small, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Disabled, + visibility = Visibility.Medium, + feeling = Feeling.Disabled, text = null, icon = R.drawable.ic_round_calculate_24 ) { diff --git a/design-system/src/main/java/com/ivy/design/l2_components/modal/ModalPreviewUtil.kt b/design-system/src/main/java/com/ivy/design/l2_components/modal/ModalPreviewUtil.kt new file mode 100644 index 0000000000..6c348be086 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l2_components/modal/ModalPreviewUtil.kt @@ -0,0 +1,6 @@ +package com.ivy.design.l2_components.modal + +import androidx.compose.runtime.Composable + +@Composable +fun previewModal(): IvyModal = rememberIvyModal().apply { show() } \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalActions.kt b/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalActions.kt index 09de9d3823..7a0c9d10d8 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalActions.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalActions.kt @@ -11,9 +11,9 @@ import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.design.l2_components.modal.IvyModal import com.ivy.design.l2_components.modal.Modal import com.ivy.design.l2_components.modal.scope.ModalActionsScope -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.IvyPreview @@ -21,14 +21,16 @@ import com.ivy.design.util.IvyPreview @Composable fun ModalActionsScope.DynamicSave( item: Any?, + hapticFeedback: Boolean = false, onClick: () -> Unit ) { IvyButton( size = ButtonSize.Small, - visibility = ButtonVisibility.Focused, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Focused, + feeling = Feeling.Positive, text = if (item != null) "Save" else "Add", icon = if (item != null) R.drawable.ic_round_check_24 else R.drawable.ic_round_add_24, + hapticFeedback = hapticFeedback, onClick = onClick ) } @@ -36,12 +38,14 @@ fun ModalActionsScope.DynamicSave( @Suppress("unused") @Composable fun ModalActionsScope.Set( + hapticFeedback: Boolean = false, onClick: () -> Unit ) { Positive( text = stringResource(R.string.set), icon = R.drawable.ic_round_check_24, - visibility = ButtonVisibility.Focused, + visibility = Visibility.Focused, + hapticFeedback = hapticFeedback, onClick = onClick ) } @@ -49,12 +53,14 @@ fun ModalActionsScope.Set( @Suppress("unused") @Composable fun ModalActionsScope.Choose( + hapticFeedback: Boolean = false, onClick: () -> Unit ) { Positive( text = "Choose", icon = R.drawable.ic_round_check_24, - visibility = ButtonVisibility.Focused, + visibility = Visibility.Focused, + hapticFeedback = hapticFeedback, onClick = onClick ) } @@ -62,12 +68,14 @@ fun ModalActionsScope.Choose( @Suppress("unused") @Composable fun ModalActionsScope.Done( + hapticFeedback: Boolean = false, onClick: () -> Unit ) { Positive( text = "Done", icon = R.drawable.ic_round_check_24, - visibility = ButtonVisibility.Focused, + visibility = Visibility.Focused, + hapticFeedback = hapticFeedback, onClick = onClick ) } @@ -79,15 +87,18 @@ fun ModalActionsScope.Positive( text: String?, @DrawableRes icon: Int? = null, - visibility: ButtonVisibility = ButtonVisibility.Focused, + visibility: Visibility = Visibility.Focused, + feeling: Feeling = Feeling.Positive, + hapticFeedback: Boolean = false, onClick: () -> Unit ) { IvyButton( size = ButtonSize.Small, visibility = visibility, - feeling = ButtonFeeling.Positive, + feeling = feeling, text = text, icon = icon, + hapticFeedback = hapticFeedback, onClick = onClick, ) } @@ -98,15 +109,17 @@ fun ModalActionsScope.Negative( text: String, @DrawableRes icon: Int? = null, - visibility: ButtonVisibility = ButtonVisibility.Focused, + visibility: Visibility = Visibility.Focused, + hapticFeedback: Boolean = false, onClick: () -> Unit ) { IvyButton( size = ButtonSize.Small, visibility = visibility, - feeling = ButtonFeeling.Negative, + feeling = Feeling.Negative, text = text, icon = icon, + hapticFeedback = hapticFeedback, onClick = onClick, ) } @@ -117,15 +130,17 @@ fun ModalActionsScope.Secondary( text: String?, @DrawableRes icon: Int? = null, - feeling: ButtonFeeling = ButtonFeeling.Positive, + feeling: Feeling = Feeling.Positive, + hapticFeedback: Boolean = false, onClick: () -> Unit ) { IvyButton( size = ButtonSize.Small, - visibility = ButtonVisibility.Medium, + visibility = Visibility.Medium, feeling = feeling, text = text, icon = icon, + hapticFeedback = hapticFeedback, onClick = onClick, ) } @@ -170,7 +185,7 @@ private fun Preview_PositiveNegative() { Modal( modal = modal, actions = { - Negative(text = "No", visibility = ButtonVisibility.High) {} + Negative(text = "No", visibility = Visibility.High) {} SpacerHor(width = 12.dp) Positive(text = "Yes") {} } diff --git a/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalSearch.kt b/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalSearch.kt new file mode 100644 index 0000000000..4e0f3d49a1 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalSearch.kt @@ -0,0 +1,155 @@ +package com.ivy.design.l2_components.modal.components + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l0_system.UI +import com.ivy.design.l2_components.input.InputFieldType +import com.ivy.design.l2_components.input.IvyInputField +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l2_components.modal.scope.ModalActionsScope +import com.ivy.design.l2_components.modal.scope.ModalScope +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.util.IvyPreview +import com.ivy.resources.R + + +@Composable +fun ModalScope.Search( + searchBarVisible: Boolean, + initialSearchQuery: String, + searchHint: String, + resetSearch: () -> Unit, + onSearch: (String) -> Unit, + overlay: (@Composable BoxScope.() -> Unit)? = null, + content: LazyListScope.() -> Unit, +) { + Box(modifier = Modifier.weight(1f)) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + content = content + ) + SearchBar( + visible = searchBarVisible, + initialQuery = initialSearchQuery, + hint = searchHint, + resetSearch = resetSearch, + onSearch = onSearch + ) + overlay?.invoke(this) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun SearchBar( + visible: Boolean, + initialQuery: String, + hint: String, + resetSearch: () -> Unit, + onSearch: (String) -> Unit, +) { + // the FocusRequester must be remembered outside AnimatedVisibility + // otherwise it crashes for no reason + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + AnimatedVisibility( + modifier = Modifier + .fillMaxWidth() + .background(UI.colors.pure) + .padding(top = 16.dp, bottom = 8.dp), + visible = visible, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + IvyInputField( + modifier = Modifier + .focusRequester(focusRequester) + .fillMaxWidth() + .padding(horizontal = 16.dp), + type = InputFieldType.SingleLine, + initialValue = initialQuery, + placeholder = hint, + iconLeft = R.drawable.round_search_24, + imeAction = ImeAction.Search, + onImeAction = { + keyboardController?.hide() + focusRequester.freeFocus() + }, + onValueChange = { onSearch(it) }, + ) + + LaunchedEffect(visible) { + if (visible) { + focusRequester.requestFocus() + keyboardController?.show() + } + } + BackHandler(enabled = visible) { + resetSearch() + } + } +} + +@Composable +fun ModalActionsScope.SearchButton( + searchBarVisible: Boolean, + onClick: () -> Unit, +) { + Secondary( + text = null, + icon = if (searchBarVisible) + R.drawable.round_search_off_24 else R.drawable.round_search_24, + feeling = if (searchBarVisible) Feeling.Negative else Feeling.Positive, + onClick = onClick, + ) +} + + +// region Previews +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = rememberIvyModal() + modal.show() + Modal( + modal = modal, + actions = { + SearchButton( + searchBarVisible = true, + ) { + + } + } + ) { + Search( + searchBarVisible = true, + initialSearchQuery = "", + searchHint = "Search hint", + resetSearch = { }, + onSearch = {} + ) { + item { + Title("Modal title") + } + } + } + } +} +// endregion \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalTitle.kt b/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalTitle.kt index 8367aea5d9..b1a48874be 100644 --- a/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalTitle.kt +++ b/design-system/src/main/java/com/ivy/design/l2_components/modal/components/ModalTitle.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.ivy.design.l0_system.UI import com.ivy.design.l1_buildingBlocks.B1 @@ -19,12 +20,14 @@ import com.ivy.design.util.IvyPreview @Composable fun ModalScope.Title( text: String, + paddingStart: Dp = 32.dp, color: Color = UI.colorsInverted.pure ) { - SpacerVer(height = 24.dp) B1( text = text, - modifier = Modifier.padding(start = 32.dp), + modifier = Modifier + .padding(start = paddingStart) + .padding(top = 24.dp), fontWeight = FontWeight.ExtraBold, color = color ) diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/BackButton.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/BackButton.kt new file mode 100644 index 0000000000..7a1f6443ac --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/BackButton.kt @@ -0,0 +1,36 @@ +package com.ivy.design.l3_ivyComponents + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.design.R +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +@Composable +fun BackButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = null, + icon = R.drawable.round_arrow_back_ios_24, + onClick = onClick, + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + BackButton {} + } +} +// endregion \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/Feeling.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/Feeling.kt new file mode 100644 index 0000000000..9ecafcb395 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/Feeling.kt @@ -0,0 +1,22 @@ +package com.ivy.design.l3_ivyComponents + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +sealed interface Feeling { + @Immutable + object Positive : Feeling + + @Immutable + object Negative : Feeling + + @Immutable + object Neutral : Feeling + + @Immutable + object Disabled : Feeling + + @Immutable + data class Custom(val color: Color) : Feeling +} \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/MoreInfoButton.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/MoreInfoButton.kt new file mode 100644 index 0000000000..4fdcbf2167 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/MoreInfoButton.kt @@ -0,0 +1,36 @@ +package com.ivy.design.l3_ivyComponents + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.design.R +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +@Composable +fun MoreInfoButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Low, + feeling = Feeling.Neutral, + text = null, + icon = R.drawable.outline_info_24, + onClick = onClick + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + MoreInfoButton {} + } +} +// endregion \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/ReorderButton.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/ReorderButton.kt new file mode 100644 index 0000000000..0cdbf240ca --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/ReorderButton.kt @@ -0,0 +1,36 @@ +package com.ivy.design.l3_ivyComponents + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.design.R +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +@Composable +fun ReorderButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = null, + icon = R.drawable.round_reorder_24, + onClick = onClick, + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview() { + ComponentPreview { + ReorderButton {} + } +} +// endregion \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/ReorderModal.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/ReorderModal.kt new file mode 100644 index 0000000000..1012db856f --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/ReorderModal.kt @@ -0,0 +1,278 @@ +import android.annotation.SuppressLint +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B1Second +import com.ivy.design.l1_buildingBlocks.IconRes +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview +import com.ivy.resources.R + +// TODO: Not refactored legacy code. It can be improved! + +@Composable +fun BoxScope.ReorderModal( + modal: IvyModal, + level: Int = 1, + items: List, + onReorder: (reordered: List) -> Unit, + itemContent: @Composable RowScope.(Int, T) -> Unit, +) { + var reorderedList = remember(items) { listOf() } + + Modal( + modal = modal, + level = level, + contentModifier = Modifier, // fixes Compose compiler crash + actions = { + Positive(text = stringResource(R.string.reorder)) { + onReorder(reorderedList) + modal.hide() + } + } + ) { + Title(text = stringResource(R.string.reorder)) + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + val mediumColor = UI.colors.medium + AndroidView( + modifier = Modifier + .fillMaxSize() + .background(UI.colors.pure) + .padding(vertical = 16.dp), + factory = { + RecyclerView(it).apply { + val itemTouchHelper = itemTouchHelper( + mediumColor = mediumColor + ) + adapter = ReorderAdapter( + itemTouchHelper = itemTouchHelper, + itemContent = itemContent, + onReorder = { reordered -> + reorderedList = reordered + } + ) + layoutManager = LinearLayoutManager(it) + itemTouchHelper.attachToRecyclerView(this) + + adapter().display(items) + } + }, + update = { + } + ) + } + } +} + + +@Suppress("UNCHECKED_CAST") +class ReorderAdapter( + private val itemTouchHelper: ItemTouchHelper, + private val itemContent: @Composable RowScope.(Int, T) -> Unit, + private val onReorder: (reordered: List) -> Unit +) : RecyclerView.Adapter.ItemViewHolder>() { + val data = mutableListOf() + + @SuppressLint("NotifyDataSetChanged") + fun display(items: List) { + data.clear() + data.addAll(items) + notifyDataSetChanged() + } + + fun moveItem(from: Int, to: Int) { + swap(from, to) + notifyItemMoved(from, to) + } + + private fun swap(from: Int, to: Int) { + val temp = data[from] + data[from] = data[to] + data[to] = temp + } + + fun onItemMoved(item: T, to: Int) { + data[to] = item + } + + fun onReorderInternalList() { + onReorder(data) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder(ComposeView(parent.context)) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.display( + item = data[position], + ItemContent = itemContent, + position = position + ) + } + + override fun getItemCount() = data.size + + inner class ItemViewHolder( + itemView: View, + ) : RecyclerView.ViewHolder(itemView) { + + fun display( + item: T, + ItemContent: @Composable RowScope.(Int, T) -> Unit, + position: Int + ) { + (itemView as ComposeView).setContent { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + SpacerHor(width = 8.dp) + IconRes( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures( + onPress = { + itemTouchHelper.startDrag(this@ItemViewHolder) + } + ) + } + .testTag("reorder_drag_handle"), + icon = R.drawable.ic_drag_handle, + tint = UI.colors.neutral, + contentDescription = "reorder_${position}" + ) + ItemContent(absoluteAdapterPosition, item) + } + } + } + } +} + + +@Suppress("UNCHECKED_CAST") +fun itemTouchHelper( + mediumColor: Color, +): ItemTouchHelper { + // 1. Note that I am specifying all 4 directions. + // Specifying START and END also allows + // more organic dragging than just specifying UP and DOWN. + val simpleItemTouchCallback = + object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + var movedItem: T? = null + var finalTo: Int? = null + + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter() + + val from = viewHolder.adapterPosition + val to = target.adapterPosition + + val targetItem = adapter.data[from] as? T ?: return false + + if (movedItem == null) { + movedItem = targetItem + } + finalTo = to + + adapter.moveItem(from, to) + + return true + } + + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int + ) { + // 4. Code block for horizontal swipe. + // ItemTouchHelper handles horizontal swipe as well, but + // it is not relevant with reordering. Ignoring here. + } + + // 1. This callback is called when a ViewHolder is selected. + // We highlight the ViewHolder here. + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) + + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + viewHolder?.itemView?.setBackgroundColor(mediumColor.toArgb()) + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.background = null + val adapter = recyclerView.adapter() + if (movedItem != null && finalTo != null) { + adapter.onItemMoved(movedItem!!, finalTo!!) + } + adapter.onReorderInternalList() + + movedItem = null + finalTo = null + } + } + return ItemTouchHelper(simpleItemTouchCallback) +} + +@Suppress("UNCHECKED_CAST") +private fun RecyclerView.adapter() = adapter as? ReorderAdapter + ?: error("Adapter not set or wrong adapter set to recyclerview.") + + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + ReorderModal( + modal = modal, + items = (1..100).toList(), + onReorder = {}, + ) { _, item -> + SpacerHor(width = 8.dp) + B1Second(text = "Number: $item") + } + } +} +// endregion diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/Visibility.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/Visibility.kt new file mode 100644 index 0000000000..bc48987722 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/Visibility.kt @@ -0,0 +1,5 @@ +package com.ivy.design.l3_ivyComponents + +enum class Visibility { + Focused, High, Medium, Low +} diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/WrapContentRow.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/WrapContentRow.kt new file mode 100644 index 0000000000..89847210cf --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/WrapContentRow.kt @@ -0,0 +1,75 @@ +package com.ivy.design.l3_ivyComponents + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + + +@Composable +fun WrapContentRow( + items: List, + itemKey: (T) -> String, + modifier: Modifier = Modifier, + horizontalMarginBetweenItems: Dp = 8.dp, + verticalMarginBetweenRows: Dp = 8.dp, + itemContent: @Composable (item: T) -> Unit +) { + if (items.isEmpty()) return + + Layout( + modifier = modifier, + content = { + for (item in items) { + key(itemKey(item)) { + itemContent(item) + } + } + } + ) { measurables, constraints -> + val childConstraints = constraints.copy( + minWidth = 0, minHeight = 0 + ) + + var x = 0 + + val placeables = measurables.map { + it.measure(childConstraints) + } + val itemHeight = placeables.maxOfOrNull { it.height } ?: 0 + + var height = 0 + + for (placeable in placeables) { + if (x + placeable.width > constraints.maxWidth) { + //item is overflowing -> move it to a new row + x = 0 + height += itemHeight + verticalMarginBetweenRows.roundToPx() + x += placeable.width + horizontalMarginBetweenItems.roundToPx() + continue + } + x += placeable.width + horizontalMarginBetweenItems.roundToPx() + } + + height += itemHeight + + layout(constraints.maxWidth, height) { + //Reset x + x = 0 + var y = 0 + + placeables.forEach { placeable -> + if (x + placeable.width > constraints.maxWidth) { + //item is overflowing -> move it to a new row + x = 0 + y += itemHeight + verticalMarginBetweenRows.roundToPx() + } + + placeable.place(x, y) + x += placeable.width + horizontalMarginBetweenItems.roundToPx() + } + } + } +} \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ArchiveButton.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ArchiveButton.kt new file mode 100644 index 0000000000..bfef3001ac --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ArchiveButton.kt @@ -0,0 +1,47 @@ +package com.ivy.design.l3_ivyComponents.button + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.design.R +import com.ivy.design.l0_system.UI +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.util.ComponentPreview + +@Composable +fun ArchiveButton( + archived: Boolean, + color: Color = UI.colors.primary, + onArchive: () -> Unit, + onUnarchive: () -> Unit +) { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Custom(color), + text = null, + icon = if (archived) R.drawable.round_unarchive_24 else R.drawable.round_archive_24 + ) { + if (archived) onUnarchive() else onArchive() + } +} + + +// region Preview +@Preview +@Composable +private fun Preview_Archive() { + ComponentPreview { + ArchiveButton(archived = false, onArchive = {}, onUnarchive = {}) + } +} + +@Preview +@Composable +private fun Preview_Unarchive() { + ComponentPreview { + ArchiveButton(archived = true, onArchive = {}, onUnarchive = {}) + } +} +// endregion \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ButtonFeeling.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ButtonFeeling.kt deleted file mode 100644 index 0b2177ea6b..0000000000 --- a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ButtonFeeling.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.design.l3_ivyComponents.button - -enum class ButtonFeeling { - Positive, Negative, Neutral, Disabled -} \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ButtonVisibility.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ButtonVisibility.kt deleted file mode 100644 index a3bc4306bf..0000000000 --- a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/ButtonVisibility.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.design.l3_ivyComponents.button - -enum class ButtonVisibility { - Focused, High, Medium, Low -} diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/DeleteButton.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/DeleteButton.kt new file mode 100644 index 0000000000..7d112c1855 --- /dev/null +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/DeleteButton.kt @@ -0,0 +1,34 @@ +package com.ivy.design.l3_ivyComponents.button + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.design.R +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.util.ComponentPreview + +@Composable +fun DeleteButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.High, + feeling = Feeling.Negative, + text = null, + icon = R.drawable.outline_delete_24, + onClick = onClick + ) +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + DeleteButton(onClick = {}) + } +} \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/IvyButton.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/IvyButton.kt index 8a75a229fe..a0272df0e2 100644 --- a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/IvyButton.kt +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/button/IvyButton.kt @@ -8,13 +8,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.design.R import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.rememberContrastColor +import com.ivy.design.l0_system.color.rememberContrast import com.ivy.design.l0_system.style import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.design.l1_buildingBlocks.SpacerVer @@ -22,6 +25,8 @@ import com.ivy.design.l1_buildingBlocks.data.solid import com.ivy.design.l1_buildingBlocks.data.solidWithBorder import com.ivy.design.l1_buildingBlocks.glow import com.ivy.design.l2_components.button.* +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.util.ComponentPreview import com.ivy.design.util.padding import com.ivy.design.util.thenIf @@ -29,63 +34,70 @@ import com.ivy.design.util.thenIf @Composable fun IvyButton( size: ButtonSize, - visibility: ButtonVisibility, - feeling: ButtonFeeling, + visibility: Visibility, + feeling: Feeling, text: String?, @DrawableRes icon: Int?, modifier: Modifier = Modifier, + shape: Shape = UI.shapes.fullyRounded, + typo: TextStyle = UI.typo.b2, + fontWeight: FontWeight = FontWeight.Bold, + hapticFeedback: Boolean = false, onClick: () -> Unit, ) { - val bgColor = when (feeling) { - ButtonFeeling.Positive -> UI.colors.primary - ButtonFeeling.Negative -> UI.colors.red - ButtonFeeling.Neutral -> UI.colors.neutral - ButtonFeeling.Disabled -> UI.colors.medium - } + val feelingColor = feeling.toColor() val iconOnly = icon != null && text == null - val padding = if (iconOnly) - padding(all = 12.dp) else padding(horizontal = 24.dp, vertical = 12.dp) - val shape = if (iconOnly) UI.shapes.circle else UI.shapes.fullyRounded + val padding = when { + iconOnly -> padding(all = 12.dp) + icon != null -> padding( + start = 12.dp, + end = 20.dp, + top = 12.dp, + bottom = 12.dp, + ) + else -> padding(horizontal = 20.dp, vertical = 12.dp) + } + val overrideShape = if (iconOnly) UI.shapes.circle else shape val background = when (visibility) { - ButtonVisibility.Focused, ButtonVisibility.High -> solid( - shape = shape, - color = bgColor, + Visibility.Focused, Visibility.High -> solid( + shape = overrideShape, + color = feelingColor, padding = padding, ) - ButtonVisibility.Medium -> solidWithBorder( - shape = shape, + Visibility.Medium -> solidWithBorder( + shape = overrideShape, solid = UI.colors.pure, borderWidth = 2.dp, - borderColor = bgColor, + borderColor = feelingColor, padding = padding, ) - ButtonVisibility.Low -> solid( - shape = shape, + Visibility.Low -> solid( + shape = overrideShape, color = UI.colors.transparent, padding = padding, ) } val textColor = when (visibility) { - ButtonVisibility.Focused, ButtonVisibility.High -> - rememberContrastColor(bgColor) - ButtonVisibility.Medium -> rememberContrastColor(UI.colors.pure) - ButtonVisibility.Low -> UI.colors.primary + Visibility.Focused, Visibility.High -> + rememberContrast(feelingColor) + Visibility.Medium -> UI.colorsInverted.pure + Visibility.Low -> feelingColor } - val textStyle = UI.typo.b2.style( + val textStyle = typo.style( color = textColor, - fontWeight = FontWeight.Bold, + fontWeight = fontWeight, textAlign = TextAlign.Center, ) val sizeModifier = when (size) { ButtonSize.Big -> modifier.fillMaxWidth() ButtonSize.Small -> modifier - }.thenIf(visibility == ButtonVisibility.Focused) { - this.glow(bgColor) + }.thenIf(visibility == Visibility.Focused) { + this.glow(feelingColor) } when { @@ -99,10 +111,11 @@ fun IvyButton( }, text = text, iconLeft = icon, - iconPadding = 12.dp, + iconPadding = 8.dp, iconTint = textColor, background = background, textStyle = textStyle, + hapticFeedback = hapticFeedback, onClick = onClick, ) } @@ -113,6 +126,7 @@ fun IvyButton( icon = icon, iconTint = textColor, background = background, + hapticFeedback = hapticFeedback, onClick = onClick ) } @@ -123,12 +137,14 @@ fun IvyButton( text = text, textStyle = textStyle, background = background, + hapticFeedback = hapticFeedback, onClick = onClick ) } } } + // region Previews @Preview @Composable @@ -138,8 +154,19 @@ private fun PreviewCommon() { IvyButton( modifier = Modifier.padding(horizontal = 16.dp), size = ButtonSize.Big, - visibility = ButtonVisibility.Focused, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Focused, + feeling = Feeling.Positive, + text = "Add", + icon = R.drawable.ic_vue_crypto_icon + ) {} + + SpacerVer(height = 12.dp) + + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Small, + visibility = Visibility.Focused, + feeling = Feeling.Positive, text = "Add", icon = R.drawable.ic_vue_crypto_icon ) {} @@ -149,8 +176,8 @@ private fun PreviewCommon() { IvyButton( modifier = Modifier.padding(horizontal = 16.dp), size = ButtonSize.Small, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Negative, + visibility = Visibility.Medium, + feeling = Feeling.Negative, text = "Error, okay?", icon = null, ) {} @@ -160,8 +187,8 @@ private fun PreviewCommon() { IvyButton( modifier = Modifier.padding(horizontal = 16.dp), size = ButtonSize.Small, - visibility = ButtonVisibility.Focused, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Focused, + feeling = Feeling.Positive, text = null, icon = R.drawable.ic_round_add_24, ) {} @@ -171,8 +198,8 @@ private fun PreviewCommon() { IvyButton( modifier = Modifier.padding(horizontal = 16.dp), size = ButtonSize.Small, - visibility = ButtonVisibility.Focused, - feeling = ButtonFeeling.Disabled, + visibility = Visibility.Focused, + feeling = Feeling.Disabled, text = "Disabled button", icon = null, ) {} @@ -182,8 +209,8 @@ private fun PreviewCommon() { IvyButton( modifier = Modifier.padding(horizontal = 16.dp), size = ButtonSize.Small, - visibility = ButtonVisibility.Low, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Low, + feeling = Feeling.Positive, text = "Text-only", icon = null ) {} @@ -198,8 +225,8 @@ private fun PreviewCommon() { IvyButton( modifier = Modifier.weight(1f), size = ButtonSize.Big, - visibility = ButtonVisibility.High, - feeling = ButtonFeeling.Positive, + visibility = Visibility.High, + feeling = Feeling.Positive, text = "Save", icon = null, ) {} @@ -209,15 +236,37 @@ private fun PreviewCommon() { IvyButton( modifier = Modifier.weight(1f), size = ButtonSize.Big, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Negative, + visibility = Visibility.Medium, + feeling = Feeling.Negative, text = "Delete", icon = null, ) {} SpacerHor(width = 16.dp) } + + SpacerVer(height = 16.dp) + + IvyButton( + modifier = Modifier.padding(start = 16.dp), + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Negative, + text = null, + icon = R.drawable.round_archive_24, + ) {} } } } -// endregion \ No newline at end of file +// endregion + +// region Utility functions +@Composable +fun Feeling.toColor(): Color = when (this) { + Feeling.Positive -> UI.colors.primary + Feeling.Negative -> UI.colors.red + Feeling.Neutral -> UI.colors.neutral + Feeling.Disabled -> UI.colors.medium + is Feeling.Custom -> color +} +// endregion diff --git a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/modal/DeleteConfirmationModal.kt b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/modal/DeleteConfirmationModal.kt index b970fa1cf3..3f1a9eaaf6 100644 --- a/design-system/src/main/java/com/ivy/design/l3_ivyComponents/modal/DeleteConfirmationModal.kt +++ b/design-system/src/main/java/com/ivy/design/l3_ivyComponents/modal/DeleteConfirmationModal.kt @@ -18,17 +18,22 @@ import com.ivy.design.util.IvyPreview @Composable fun BoxScope.DeleteConfirmationModal( modal: IvyModal, + level: Int = 1, message: String = "Are you sure that you want to delete it forever? " + "Once deleted, it can NOT be undone.", onDelete: () -> Unit, ) { Modal( modal = modal, + level = level, actions = { Negative( text = "Delete", icon = R.drawable.ic_round_delete_forever_24, - onClick = onDelete + onClick = { + modal.hide() + onDelete() + } ) } ) { diff --git a/design-system/src/main/java/com/ivy/design/util/Insets.kt b/design-system/src/main/java/com/ivy/design/util/Insets.kt index 26fcc5645d..209005972a 100644 --- a/design-system/src/main/java/com/ivy/design/util/Insets.kt +++ b/design-system/src/main/java/com/ivy/design/util/Insets.kt @@ -1,10 +1,45 @@ package com.ivy.design.util import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Dp import androidx.core.graphics.Insets import androidx.core.view.WindowInsetsCompat +// region Insets Compose +/** + * @return system's bottom inset (nav buttons or bottom nav) + */ +@Composable +fun systemPaddingBottom(): Dp { + val rootView = LocalView.current + val densityScope = LocalDensity.current + return remember(rootView) { + val insetPx = + WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets, rootView) + .getInsets(WindowInsetsCompat.Type.navigationBars()) + .bottom + with(densityScope) { insetPx.toDp() } + } +} + +@Composable +fun keyboardPadding(): Dp { + val rootView = LocalView.current + val insetPx = + WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets, rootView) + .getInsets( + WindowInsetsCompat.Type.ime() or + WindowInsetsCompat.Type.navigationBars() + ) + .bottom + return insetPx.toDensityDp() +} +// endregion + + @Composable fun windowInsets(): WindowInsetsCompat { val rootView = LocalView.current diff --git a/design-system/src/main/java/com/ivy/design/util/Keyboard.kt b/design-system/src/main/java/com/ivy/design/util/Keyboard.kt index 2d671ce2a6..92f7690e35 100644 --- a/design-system/src/main/java/com/ivy/design/util/Keyboard.kt +++ b/design-system/src/main/java/com/ivy/design/util/Keyboard.kt @@ -1,102 +1,60 @@ package com.ivy.design.util -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context import android.view.View -import android.view.inputmethod.InputMethodManager -import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animateDpAsState import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnLayout -@SuppressLint("ComposableNaming") @Composable -fun showKeyboard() { - LocalView.current.showKeyboard() -} - -fun View.showKeyboard() { - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(this, InputMethodManager.SHOW_FORCED) -} - -@SuppressLint("ComposableNaming") -@Composable -fun hideKeyboard() { - LocalView.current.hideKeyboard() -} - -fun View.hideKeyboard() { - val imm: InputMethodManager = - context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) -} - -@Composable -fun keyboardHeightStateAnimated( - animationSpec: AnimationSpec = springBounce() -): State { - val keyboardVisible by keyboardVisibleState() - +fun keyboardShiftAnimated(): State { + val systemBottomPadding = systemPaddingBottom() + val keyboardShown by keyboardShownState() + val keyboardShownInset = keyboardPadding() return animateDpAsState( - animationSpec = animationSpec, - targetValue = if (keyboardVisible) - keyboardOnlyWindowInsets().bottom.toDensityDp() else 0.dp + targetValue = if (keyboardShown) + keyboardShownInset else systemBottomPadding, ) } @Composable -fun keyboardHeightState(): State { - val keyboardVisible by keyboardVisibleState() - val keyboardHeight = if (keyboardVisible) - keyboardOnlyWindowInsets().bottom.toDensityDp() else 0.dp - return remember(keyboardHeight) { - mutableStateOf(keyboardHeight) - } -} - -@Composable -fun keyboardVisibleState(): State { +fun keyboardShownState(): State { + val keyboardOpen = remember { mutableStateOf(false) } val rootView = LocalView.current - val keyboardVisible = remember { - mutableStateOf(false) - } + DisposableEffect(Unit) { + val keyboardListener = { + // check keyboard state after this layout + val isOpenNew = isKeyboardOpen(rootView) - onEvent { - rootView.addKeyboardListener { - keyboardVisible.value = it + // since the observer is hit quite often, only callback when there is a change. + if (isOpenNew != keyboardOpen.value) { + keyboardOpen.value = isOpenNew + } } - } - return keyboardVisible -} - -fun View.addKeyboardListener(keyboardCallback: (visible: Boolean) -> Unit) { - doOnLayout { - //get init state of keyboard - var keyboardVisible = isKeyboardOpen(this) + rootView.doOnLayout { + // get initial state of keyboard + keyboardOpen.value = isKeyboardOpen(rootView) - //callback as soon as the layout is set with whether the keyboard is open or not - keyboardCallback(keyboardVisible) + // whenever the layout resizes/changes, callback with the state of the keyboard. + rootView.viewTreeObserver.addOnGlobalLayoutListener(keyboardListener) + } - //whenever the layout resizes/changes, callback with the state of the keyboard. - viewTreeObserver.addOnGlobalLayoutListener { - val keyboardUpdateCheck = isKeyboardOpen(this) - //since the observer is hit quite often, only callback when there is a change. - if (keyboardUpdateCheck != keyboardVisible) { - keyboardCallback(keyboardUpdateCheck) - keyboardVisible = keyboardUpdateCheck - } + onDispose { + // stop keyboard updates + rootView.viewTreeObserver.removeOnGlobalLayoutListener(keyboardListener) } } + + return keyboardOpen } + fun isKeyboardOpen(rootView: View): Boolean { return try { WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets, rootView) @@ -105,4 +63,37 @@ fun isKeyboardOpen(rootView: View): Boolean { e.printStackTrace() false } +} + +class KeyboardController { + private val state = mutableStateOf(0) + + @OptIn(ExperimentalComposeUiApi::class) + @Composable + fun wire() { + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(state.value) { + if (state.value > 0) { + keyboardController?.show() + } else { + keyboardController?.hide() + } + } + } + + fun show() { + if (state.value > 0) { + state.value = state.value + 1 + } else { + state.value = 1 + } + } + + fun hide() { + if (state.value < 0) { + state.value = state.value - 1 + } else { + state.value = -1 + } + } } \ No newline at end of file diff --git a/design-system/src/main/java/com/ivy/design/util/Preview.kt b/design-system/src/main/java/com/ivy/design/util/Preview.kt index 951b579851..62ce8ba21f 100644 --- a/design-system/src/main/java/com/ivy/design/util/Preview.kt +++ b/design-system/src/main/java/com/ivy/design/util/Preview.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel -import com.ivy.design.Theme +import com.ivy.data.Theme import com.ivy.design.api.IvyDesign import com.ivy.design.api.IvyUI import com.ivy.design.api.setAppDesign @@ -51,8 +51,10 @@ fun IvyPreview( } @Composable -inline fun hiltViewmodelPreviewSafe(): VM? = - if (isInPreview()) null else hiltViewModel() +inline fun hiltViewModelPreviewSafe( + key: String? = null, +): VM? = + if (isInPreview()) null else hiltViewModel(key = key) @Composable fun isInPreview(): Boolean = LocalInspectionMode.current \ No newline at end of file diff --git a/docs/resources/Books.md b/docs/resources/Books.md index e9ceea6839..df17566814 100644 --- a/docs/resources/Books.md +++ b/docs/resources/Books.md @@ -7,3 +7,4 @@ - **[Designing Data-Intensive Applications](https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321)** by [Martin Kleppmann](https://www.google.com/search?q=martin+kleppmann&oq=Martin+Kleppmann) - **[Unit Testing Principles, Practices, and Patterns](https://www.amazon.com/gp/aw/d/B09782L692/ref=tmm_kin_swatch_0?ie=UTF8&qid=&sr=)** by [Vladimir Khorikov](https://www.google.com/search?q=vladimir+khorikov) - **[Domain Modeling Made Functional](https://www.amazon.com/Domain-Modeling-Made-Functional-Domain-Driven/dp/1680502549)** by [Scott Wlaschin](https://www.google.com/search?q=Scott+Wlaschin) +- **[Introduction to Algorithms, 4th edition](https://www.amazon.com/Introduction-Algorithms-fourth-Thomas-Cormen/dp/026204630X)** by [Thomas H. Cormen](https://www.google.com/search?q=thomas+h.+cormen&oq=Thomas+H.+Cormen) diff --git a/donate/README.md b/donate/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/donate/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/donate/build.gradle.kts b/donate/build.gradle.kts deleted file mode 100644 index 076fb39cd6..0000000000 --- a/donate/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` - `kotlin-android` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - - implementation(project(":billing")) -} \ No newline at end of file diff --git a/donate/src/main/AndroidManifest.xml b/donate/src/main/AndroidManifest.xml deleted file mode 100644 index e675cee6fe..0000000000 --- a/donate/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/donate/src/main/java/com/ivy/donate/DonateEvent.kt b/donate/src/main/java/com/ivy/donate/DonateEvent.kt deleted file mode 100644 index ef6f6d4ac6..0000000000 --- a/donate/src/main/java/com/ivy/donate/DonateEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.ivy.donate - -import android.app.Activity -import com.ivy.donate.data.DonateOption - -sealed class DonateEvent { - data class Load(val activity: Activity) : DonateEvent() - - data class Donate( - val activity: Activity, - val option: DonateOption - ) : DonateEvent() -} \ No newline at end of file diff --git a/donate/src/main/java/com/ivy/donate/DonateScreen.kt b/donate/src/main/java/com/ivy/donate/DonateScreen.kt deleted file mode 100644 index 6f2cde9aee..0000000000 --- a/donate/src/main/java/com/ivy/donate/DonateScreen.kt +++ /dev/null @@ -1,296 +0,0 @@ -package com.ivy.donate - -import android.app.Activity -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.Black -import com.ivy.design.l0_system.color.White -import com.ivy.design.l0_system.style -import com.ivy.design.l1_buildingBlocks.ColumnRoot -import com.ivy.design.l1_buildingBlocks.IvyText -import com.ivy.design.l1_buildingBlocks.SpacerHor -import com.ivy.design.l1_buildingBlocks.SpacerVer -import com.ivy.design.l1_buildingBlocks.data.outlined -import com.ivy.design.l2_components.button.Btn -import com.ivy.design.l2_components.button.Icon -import com.ivy.design.util.IvyPreview -import com.ivy.design.util.padding -import com.ivy.donate.data.DonateOption - - -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.components.IvyButton - -@Composable -fun BoxWithConstraintsScope.DonateScreen() { - //TODO: For some weird reason FRP<>() crashes, so I workaround it -// FRP( -// initialEvent = DonateEvent.Load(LocalContext.current as Activity) -// ) { _, onEvent -> -// UI(onEvent) -// } - val viewModel: DonateViewModel = hiltViewModel() -// val state by viewModel.state().collectAsState() - - val activity = LocalContext.current as Activity - - UI(viewModel::onEvent) -} - -@Composable -private fun BoxWithConstraintsScope.UI( - onEvent: (DonateEvent) -> Unit -) { - var donateOption by remember { mutableStateOf(DonateOption.DONATE_5) } - - Column { - Image( - modifier = Modifier.fillMaxWidth(), - painter = painterResource(id = R.drawable.donate_illustration), - contentDescription = "rocket illustration", - contentScale = ContentScale.FillWidth - ) - - ScreenContent() - } - - ColumnRoot { - SpacerVer(height = 16.dp) - - - Btn.Icon( - modifier = Modifier.padding(start = 16.dp), - icon = R.drawable.ic_back_android, - background = outlined( - width = 2.dp, - color = White, - shape = CircleShape, - padding = padding(all = 12.dp) - ) - ) { - - } - - SpacerVer(height = 16.dp) - - IvyText( - modifier = Modifier.padding(start = 24.dp), - text = "Donate", - typo = UI.typo.h2.style( - fontWeight = FontWeight.Bold, - color = White - ) - ) - - SpacerVer(height = 4.dp) - - DonateOptionPicker(option = donateOption) { - donateOption = it - } - } - - val context = LocalView.current.context - DonateButton { - onEvent(DonateEvent.Donate(context as Activity, donateOption)) - } -} - -@Composable -private fun DonateOptionPicker( - option: DonateOption, - onSelect: (DonateOption) -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - SpacerHor(width = 16.dp) - - if (option != DonateOption.DONATE_2) { - OptionPickerButton( - icon = R.drawable.ic_donate_minus, - contentDescription = "btn_minus" - ) { - val newOption = when (option) { - DonateOption.DONATE_2 -> DonateOption.DONATE_2 - DonateOption.DONATE_5 -> DonateOption.DONATE_2 - DonateOption.DONATE_10 -> DonateOption.DONATE_5 - DonateOption.DONATE_15 -> DonateOption.DONATE_10 - DonateOption.DONATE_25 -> DonateOption.DONATE_15 - DonateOption.DONATE_50 -> DonateOption.DONATE_25 - DonateOption.DONATE_100 -> DonateOption.DONATE_50 - } - onSelect(newOption) - } - } - - SpacerHor(width = 12.dp) - - IvyText( - modifier = Modifier.testTag("donation_amount"), - text = "$${ - when (option) { - DonateOption.DONATE_2 -> 2 - DonateOption.DONATE_5 -> 5 - DonateOption.DONATE_10 -> 10 - DonateOption.DONATE_15 -> 15 - DonateOption.DONATE_25 -> 25 - DonateOption.DONATE_50 -> 50 - DonateOption.DONATE_100 -> 100 - } - }", - typo = UI.typoSecond.h1.style( - fontWeight = FontWeight.Bold, - color = White - ) - ) - - SpacerHor(width = 12.dp) - - if (option != DonateOption.DONATE_100) { - OptionPickerButton( - icon = R.drawable.ic_donate_plus, - contentDescription = "btn_plus" - ) { - val newOption = when (option) { - DonateOption.DONATE_2 -> DonateOption.DONATE_5 - DonateOption.DONATE_5 -> DonateOption.DONATE_10 - DonateOption.DONATE_10 -> DonateOption.DONATE_15 - DonateOption.DONATE_15 -> DonateOption.DONATE_25 - DonateOption.DONATE_25 -> DonateOption.DONATE_50 - DonateOption.DONATE_50 -> DonateOption.DONATE_100 - DonateOption.DONATE_100 -> DonateOption.DONATE_100 - } - onSelect(newOption) - } - } - } -} - -@Composable -private fun OptionPickerButton( - @DrawableRes icon: Int, - contentDescription: String, - onClick: () -> Unit -) { - Image( - modifier = Modifier - .clip(UI.shapes.squared) - .background(Black) - .clickable { onClick() } - .padding(horizontal = 8.dp, vertical = 4.dp), - painter = painterResource(icon), - contentDescription = contentDescription, - ) -} - -@Composable -private fun ScreenContent() { - LazyColumn { - item { - SpacerVer(height = 32.dp) - - IvyText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp), - text = "It seems like you enjoy free and open-source software. We too!", - typo = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) - } - - item { - SpacerVer(height = 12.dp) - - IvyText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp), - text = "BIG THANKS to all Ivy contributors who made Ivy Wallet possible! That's why we opened a donations channel to sustain and improve our small project.", - typo = UI.typo.b2.style( - color = UI.colors.neutral, - fontWeight = FontWeight.Medium - ) - ) - } - - item { - SpacerVer(height = 24.dp) - - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .background(UI.colors.medium, UI.shapes.squared) - .padding(horizontal = 24.dp, vertical = 16.dp), - text = "If you want to support us feel free to donate whatever amount you're comfortable with - it all helps! (local taxes may apply)".uppercase(), - style = UI.typo.c.style( - fontWeight = FontWeight.Bold, - color = UI.colorsInverted.red - ) - ) - } - - item { - SpacerVer(height = 120.dp) //scroll hack - } - } -} - -@Composable -private fun BoxWithConstraintsScope.DonateButton( - onClick: () -> Unit -) { - IvyButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .navigationBarsPadding() - .padding(horizontal = 20.dp) - .padding(bottom = 16.dp) - .testTag("btn_donate"), - iconStart = R.drawable.ic_donate_crown, - wrapContentMode = false, - iconTint = UI.colors.pure, - iconEdgePadding = 16.dp, - text = "Donate", - backgroundGradient = Gradient.solid(UI.colorsInverted.pure), - textStyle = UI.typo.b1.style( - fontWeight = FontWeight.Bold, - color = UI.colors.pure - ) - ) { - onClick() - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - UI(onEvent = {}) - } -} \ No newline at end of file diff --git a/donate/src/main/java/com/ivy/donate/DonateState.kt b/donate/src/main/java/com/ivy/donate/DonateState.kt deleted file mode 100644 index 9a2b6a7666..0000000000 --- a/donate/src/main/java/com/ivy/donate/DonateState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.ivy.donate - -sealed class DonateState { - object Success : DonateState() - - data class Error( - val errMsg: String - ) : DonateState() -} \ No newline at end of file diff --git a/donate/src/main/java/com/ivy/donate/DonateViewModel.kt b/donate/src/main/java/com/ivy/donate/DonateViewModel.kt deleted file mode 100644 index bda4fd4421..0000000000 --- a/donate/src/main/java/com/ivy/donate/DonateViewModel.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.ivy.donate - -import androidx.lifecycle.viewModelScope -import com.ivy.billing.IvyBilling -import com.ivy.billing.Plan -import com.ivy.donate.data.DonateOption -import com.ivy.frp.then -import com.ivy.frp.viewmodel.FRPViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@HiltViewModel -class DonateViewModel @Inject constructor( - private val ivyBilling: IvyBilling -) : FRPViewModel() { - override val _state: MutableStateFlow = MutableStateFlow(DonateState.Success) - - val plans = mutableListOf() - - override suspend fun handleEvent(event: DonateEvent): suspend () -> DonateState = when (event) { - is DonateEvent.Load -> load(event) - is DonateEvent.Donate -> donate(event) - } - - private fun load(event: DonateEvent.Load) = suspend { - ivyBilling.init( - activity = event.activity, - onReady = { - viewModelScope.launch { - plans.clear() - plans.addAll(ivyBilling.fetchOneTimePlans()) - } - }, - onError = { code, msg -> - updateStateNonBlocking { - DonateState.Error(errMsg = "Google Play Billing error: $code - $msg") - } - }, - onPurchases = {} - ) - DonateState.Success - } - - private fun donate(event: DonateEvent.Donate) = suspend { - when (event.option) { - DonateOption.DONATE_2 -> IvyBilling.DONATE_2 - DonateOption.DONATE_5 -> IvyBilling.DONATE_5 - DonateOption.DONATE_10 -> IvyBilling.DONATE_10 - DonateOption.DONATE_15 -> IvyBilling.DONATE_15 - DonateOption.DONATE_25 -> IvyBilling.DONATE_25 - DonateOption.DONATE_50 -> IvyBilling.DONATE_50 - DonateOption.DONATE_100 -> IvyBilling.DONATE_100 - } - } then { targetSku -> - Timber.i("Donating to sku \"$targetSku\"") - plans.find { it.sku == targetSku } - } then { plan -> - if (plan != null) { - ivyBilling.buy( - activity = event.activity, - skuToBuy = plan.skuDetails, - oldSubscriptionPurchaseToken = null - ) - } - stateVal() - } -} \ No newline at end of file diff --git a/donate/src/main/java/com/ivy/donate/data/DonateOption.kt b/donate/src/main/java/com/ivy/donate/data/DonateOption.kt deleted file mode 100644 index 0fbd79e74d..0000000000 --- a/donate/src/main/java/com/ivy/donate/data/DonateOption.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.donate.data - -enum class DonateOption { - DONATE_2, DONATE_5, DONATE_10, DONATE_15, DONATE_25, DONATE_50, DONATE_100 -} \ No newline at end of file diff --git a/balance-prediction/.gitignore b/drive/google-drive/.gitignore similarity index 100% rename from balance-prediction/.gitignore rename to drive/google-drive/.gitignore diff --git a/drive/google-drive/build.gradle.kts b/drive/google-drive/build.gradle.kts new file mode 100644 index 0000000000..a0bec182e6 --- /dev/null +++ b/drive/google-drive/build.gradle.kts @@ -0,0 +1,31 @@ +import com.ivy.buildsrc.Google +import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing +import com.ivy.buildsrc.Timber + +apply() + +plugins { + `android-library` +} + +dependencies { + implementation(project(":common:main")) + + // region Google Drive deps + // TODO: Extract to "dependencies.gradle.kts" in buildSrc + implementation("com.google.apis:google-api-services-drive:v3-rev136-1.25.0") { + exclude(group = "com.google.guava", module = "listenablefuture") + } + implementation("com.google.http-client:google-http-client-gson:1.26.0") + implementation("com.google.api-client:google-api-client-android:1.26.0") { + exclude(group = "com.google.guava", module = "listenablefuture") + } + implementation("com.google.guava:guava:28.1-android") + // endregion + + Hilt() + Google() + Timber(api = true) + Testing() +} \ No newline at end of file diff --git a/drive/google-drive/src/main/AndroidManifest.xml b/drive/google-drive/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..f99a47898f --- /dev/null +++ b/drive/google-drive/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveFileType.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveFileType.kt new file mode 100644 index 0000000000..80d0ef9b72 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveFileType.kt @@ -0,0 +1,20 @@ +package com.ivy.drive.google_drive.data + +sealed class GoogleDriveFileType { + class Backup(val type: GoogleDriveType = GoogleDriveType.BackupTypeCSV): GoogleDriveFileType(){ + companion object { + const val FOLDER_NAME = "Backup" + } + } + class Image(val type: GoogleDriveType = GoogleDriveType.ImageTypeJPEG): GoogleDriveFileType(){ + companion object { + const val FOLDER_NAME = "Image" + } + } + class Video(val type: GoogleDriveType = GoogleDriveType.VideoType): GoogleDriveFileType(){ + companion object { + const val FOLDER_NAME = "Video" + } + } + +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceHelper.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceHelper.kt new file mode 100644 index 0000000000..94b3bb7ae0 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceHelper.kt @@ -0,0 +1,167 @@ +package com.ivy.drive.google_drive.data + +import com.google.api.client.http.FileContent +import com.google.api.services.drive.Drive +import com.google.api.services.drive.model.File +import com.google.api.services.drive.model.FileList +import com.ivy.drive.google_drive.data.GoogleDriveFileType.Image +import com.ivy.drive.google_drive.data.GoogleDriveFileType.Backup +import com.ivy.drive.google_drive.data.GoogleDriveFileType.Video +import com.ivy.drive.google_drive.util.GoogleDriveUtil +import com.ivy.drive.google_drive.util.GoogleDriveUtil.IVY_WALLET_ROOT_FOLDER +import com.ivy.drive.google_drive.util.GoogleDriveUtil.createGoogleDriveFile +import com.ivy.drive.google_drive.util.GoogleDriveUtil.getParents +import com.ivy.drive.google_drive.util.GoogleDriveUtil.ivyRootFolderParent +import com.ivy.drive.google_drive.util.GoogleDriveUtil.updateGoogleDriveFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import timber.log.Timber +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream + +internal class GoogleDriveServiceHelper(private val drive: Drive) { + + fun uploadFileAsync( + file: File, + fileContent: FileContent? = null, + driveFiletype: GoogleDriveFileType + ): Deferred { + + return CoroutineScope(Dispatchers.IO).async { + val currentFile = getFileByName(file.name) + + if(currentFile != null) { + Timber.d("Current File not null") + val parentFolder = checkIfFolderExist(driveFiletype.toString()) + if (fileContent != null) { + updateFile( + fileId = currentFile.id, + driveType = driveFiletype, + fileName = currentFile.name, + parent = parentFolder, + fileContent = fileContent + ) + } else { + "Couldn't upload the file as contents were empty" + } + } else { + Timber.d("Current File null creating new file") + + createFile( + fileContent = fileContent, + fileName = file.name, + driveType = driveFiletype + ) + } + } + } + + fun downloadFile(fileName: String): OutputStream? { + val file = getFileByName(fileName) + val outputStream = ByteArrayOutputStream() + if(file != null) { + drive.files().get(file.id).executeAndDownloadTo(outputStream) + return outputStream + } + return null + } + + private fun updateFile( + fileId: String, + fileContent: FileContent, + fileName: String, + parent: String?, + driveType: GoogleDriveFileType + ): String { + val type = driveType.toString() + val fileMetadata = updateGoogleDriveFile(fileName,type) + val googleFile = drive.files().update(fileId,fileMetadata,fileContent).setAddParents(parent).execute() + ?: throw IOException("Error while updating and uploading google drive file") + return googleFile.id + } + + private fun createFile( + fileContent: FileContent?, + fileName: String, + driveType: GoogleDriveFileType + ): String { + val type = when(driveType) { + is Backup -> driveType.type + is Image -> driveType.type + is Video -> driveType.type + } + + val typeValue = when(type) { + is GoogleDriveType.BackupTypeCSV -> type.VALUE + is GoogleDriveType.ImageTypeJPEG -> type.VALUE + is GoogleDriveType.ImageTypePNG -> type.VALUE + is GoogleDriveType.ImageTypeWEBP -> type.VALUE + is GoogleDriveType.BackupTypePlainText -> type.VALUE + else -> { + GoogleDriveType.BackupTypePlainText.VALUE + } + } + val parentId = checkIfFolderExist(driveType.toString()) + val parents = if(parentId != null) { + getParents(parentId) + } else { + val rootId = checkIfFolderExist(IVY_WALLET_ROOT_FOLDER) + val rootParents = + if(rootId != null) { + getParents(rootId) + } else { + val id = createFolder() + getParents(id) + } + val id = createFolder(rootParents,driveType.toString()) + getParents(id) + } + val fileMetadata = createGoogleDriveFile(fileName,parents,typeValue) + + val googleFile = + if(fileContent == null) { + drive.files().create(fileMetadata).execute() + ?: throw IOException("Error while creating and uploading google drive file") + } else { + drive.files().create(fileMetadata, fileContent).execute() + ?: throw IOException("Error while creating and uploading google drive file") + } + return googleFile.id + } + + private fun getFileByName(fileName: String): File? { + val fileList = getAllFiles() + if(fileList.isEmpty().not()) { + return fileList.files.firstOrNull { it.name == fileName } + } + return null + } + + private fun getAllFiles(): FileList { + return drive.files().list().setSpaces(GoogleDriveUtil.DRIVE_SPACE).execute() + ?: throw Exception ("Error in querying the files from the drive") + } + + private fun checkIfFolderExist(fileName: String): String? { + val files = getAllFiles() + val file = files.files.firstOrNull { it.name == fileName } + return file?.id + } + + private fun createFolder( + parents: List = ivyRootFolderParent, + folderName: String = IVY_WALLET_ROOT_FOLDER + ): String { + val metadata = createGoogleDriveFile( + folderName, + parents, + GoogleDriveUtil.MIME_TYPE_FOLDER + ) + val googleFile = drive.files().create(metadata).execute() + ?: throw IOException("Error when requesting file creation.") + return googleFile.id + } +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceImpl.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceImpl.kt new file mode 100644 index 0000000000..a9981a0b0f --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceImpl.kt @@ -0,0 +1,119 @@ +package com.ivy.drive.google_drive.data + + +import android.content.Context +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.Scope +import com.google.api.client.extensions.android.http.AndroidHttp +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential +import com.google.api.client.http.FileContent +import com.google.api.client.json.jackson2.JacksonFactory +import com.google.api.services.drive.Drive +import com.google.api.services.drive.DriveScopes +import com.ivy.drive.google_drive.domain.GoogleDriveService +import com.google.api.services.drive.model.File +import com.ivy.drive.google_drive.util.GoogleDriveUtil.APP_NAME +import timber.log.Timber +import java.io.OutputStream + +internal class GoogleDriveServiceImpl : GoogleDriveService { + + private var driveHelper: GoogleDriveServiceHelper? = null + + override suspend fun upload( + file: File, + fileContent: FileContent, + driveFiletype: GoogleDriveFileType + ): String { + return try{ + driveHelper!!.uploadFileAsync( + file = file, + fileContent = fileContent, + driveFiletype = driveFiletype + ).await() + } catch (e: Exception) { + "Error while uploading a file : ${e.printStackTrace()}" + } + } + + override suspend fun download(fileName: String): OutputStream? { + return driveHelper!!.downloadFile(fileName) + } + + override fun handleSignInResult(context: Context) { + initDriveHelper(context) + } + + override fun requestSignIn(): GoogleSignInOptions { + return handleRequestSignIn() + } + + + private fun handleRequestSignIn(): GoogleSignInOptions { + Timber.d("Requesting sign-in") + return GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken("364763737033-t1d2qe7s0s8597k7anu3sb2nq79ot5tp.apps.googleusercontent.com") + .requestEmail() + .requestProfile() + .requestScopes(Scope(DriveScopes.DRIVE_FILE)) + .build() + } + + private fun initDriveHelper(context: Context) { + GoogleSignIn.getLastSignedInAccount(context).let { googleSignInAccount -> + + // Use the authenticated account to sign in to the Drive service. + val credential = GoogleAccountCredential.usingOAuth2( + context, listOf(DriveScopes.DRIVE_FILE) + ) + Timber.d("googleSignInAccount value $googleSignInAccount") + if (googleSignInAccount != null) { + credential.selectedAccount = googleSignInAccount.account + } + val googleDriveService = Drive.Builder( + AndroidHttp.newCompatibleTransport(), + JacksonFactory.getDefaultInstance(), + credential + ) + .setApplicationName(APP_NAME) + .build() + + // The DriveServiceHelper encapsulates all REST API and SAF functionality. + // Its instantiation is required before handling any onClick actions. + driveHelper = GoogleDriveServiceHelper(googleDriveService) + + } + } + + +// private fun mockFilesAndUpload() { +// CoroutineScope(Dispatchers.IO).launch { +// val file = createGoogleDriveFile( +// "IvyImageFile", +// getParents(GoogleDriveFileType.Image.FOLDER_NAME), +// GoogleDriveType.BackupTypePlainText.VALUE +// ) +// +// println(GoogleDriveFileType.Backup().toString()) +// val content = "Hey GDrive file Upload was a success Congrats!!" +// +// val textFile = java.io.File(content) +// val fileContents = FileContent("text/plain",textFile) +// +// Timber.d("Uploading the file") +// +// val result = upload( +// file = file, +// fileContent = fileContents, +// driveFiletype = GoogleDriveFileType.Image( +// GoogleDriveType.BackupTypePlainText +// ) +// ) +// +// Timber.d("File uploaded with id : $result") +// +// } +// } + +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveType.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveType.kt new file mode 100644 index 0000000000..151da2287e --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveType.kt @@ -0,0 +1,22 @@ +package com.ivy.drive.google_drive.data + +sealed interface GoogleDriveType { + object BackupTypeCSV: GoogleDriveType { + const val VALUE = "text/csv" + } + object BackupTypePlainText: GoogleDriveType { + const val VALUE = "text/plain" + } + object ImageTypePNG: GoogleDriveType { + const val VALUE = "image/png" + } + object ImageTypeJPEG: GoogleDriveType { + const val VALUE = "image/jpeg" + } + object ImageTypeWEBP: GoogleDriveType { + const val VALUE = "image/webp" + } + object VideoType: GoogleDriveType { + const val VALUE = "video/*" + } +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/di/GoogleDriveModuleDI.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/di/GoogleDriveModuleDI.kt new file mode 100644 index 0000000000..717fa3d9f7 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/di/GoogleDriveModuleDI.kt @@ -0,0 +1,22 @@ +package com.ivy.drive.google_drive.di + +import com.google.api.services.drive.Drive +import com.ivy.drive.google_drive.data.GoogleDriveServiceHelper +import com.ivy.drive.google_drive.data.GoogleDriveServiceImpl +import com.ivy.drive.google_drive.domain.GoogleDriveService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object GoogleDriveModuleDI { + + @Singleton + @Provides + fun provideGoogleDriveService(): GoogleDriveService = + GoogleDriveServiceImpl() + +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/domain/GoogleDriveService.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/domain/GoogleDriveService.kt new file mode 100644 index 0000000000..7fdc7c0e2b --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/domain/GoogleDriveService.kt @@ -0,0 +1,36 @@ +package com.ivy.drive.google_drive.domain + +import android.content.Context +import android.content.Intent +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.api.client.http.FileContent +import com.google.api.services.drive.model.File +import com.ivy.drive.google_drive.data.GoogleDriveFileType +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.OutputStream + +interface GoogleDriveService { + + companion object { + const val IVY_DEFAULT_BACKUP_FILE_NAME: String = "IvyBackupFile" + const val IVY_DEFAULT_IMAGE_FILE_NAME: String = "IvyImageFile" + } + + suspend fun upload( + file: File, + fileContent: FileContent, + driveFiletype: GoogleDriveFileType + ): String + + // TODO: Refactor how to download a file + suspend fun download( + fileName: String + ): OutputStream? + + fun handleSignInResult( + context: Context + ) + + fun requestSignIn(): + GoogleSignInOptions +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/util/GoogleDriveUtil.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/util/GoogleDriveUtil.kt new file mode 100644 index 0000000000..72656ac12e --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/util/GoogleDriveUtil.kt @@ -0,0 +1,38 @@ +package com.ivy.drive.google_drive.util + +import com.google.api.services.drive.model.File + +object GoogleDriveUtil { + const val DRIVE_SPACE = "drive" + const val IVY_WALLET_ROOT_FOLDER = "IvyWallet" + const val IVY_WALLET_BACKUP_FOLDER = "Backups" + const val IVY_WALLET_BACKUP_FILENAME = "IvyWalletBackup" + const val APP_NAME = "IvyAndroidApp" + const val MIME_TYPE_FOLDER = "application/vnd.google-apps.folder" + val ivyRootFolderParent = listOf("root") + + // TODO: Make this private + fun createGoogleDriveFile( + fileName: String, + parents: List, + type: String + ): File { + return File() + .setName(fileName) + .setParents(parents) + .setMimeType(type) + } + + fun updateGoogleDriveFile( + fileName: String, + type: String + ): File { + return File() + .setName(fileName) + .setMimeType(type) + } + + fun getParents(fileId: String): List { + return listOf(fileId) + } +} \ No newline at end of file diff --git a/budgets/.gitignore b/formula/domain/.gitignore similarity index 100% rename from budgets/.gitignore rename to formula/domain/.gitignore diff --git a/formula/README.md b/formula/domain/README.md similarity index 100% rename from formula/README.md rename to formula/domain/README.md diff --git a/formula/build.gradle.kts b/formula/domain/build.gradle.kts similarity index 83% rename from formula/build.gradle.kts rename to formula/domain/build.gradle.kts index 799fa9d60d..647e6b8388 100644 --- a/formula/build.gradle.kts +++ b/formula/domain/build.gradle.kts @@ -1,4 +1,5 @@ import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing apply() @@ -11,4 +12,5 @@ dependencies { Hilt() implementation(project(":common:main")) implementation(project(":core:domain")) + Testing() } \ No newline at end of file diff --git a/formula/domain/src/main/AndroidManifest.xml b/formula/domain/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..dd0e58c635 --- /dev/null +++ b/formula/domain/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/action/DataSourceFlow.kt b/formula/domain/src/main/java/com/ivy/formula/domain/action/DataSourceFlow.kt new file mode 100644 index 0000000000..9c33054d3f --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/action/DataSourceFlow.kt @@ -0,0 +1,40 @@ +package com.ivy.formula.domain.action + +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.calculate.CalculateFlow +import com.ivy.core.domain.action.transaction.TrnsFlow +import com.ivy.formula.domain.data.source.CalculationThing +import com.ivy.formula.domain.data.source.CalculationType +import com.ivy.formula.domain.data.source.DataSource +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class DataSourceFlow @Inject constructor( + private val trnsFlow: TrnsFlow, + private val calculateFlow: CalculateFlow +) : FlowAction() { + @OptIn(FlowPreview::class) + override fun DataSource.createFlow(): Flow = trnsFlow(filter).flatMapLatest { trns -> + calculateFlow( + CalculateFlow.Input( + trns = trns, + includeTransfers = false, + includeHidden = false, + outputCurrency = calculation.outputCurrency + ) + ) + }.map { stats -> + val byValue = calculation.type == CalculationType.ByValue + when (calculation.thing) { + CalculationThing.Income -> + if (byValue) stats.income.amount else stats.incomesCount + CalculationThing.Expense -> + if (byValue) stats.expense.amount else stats.expensesCount + CalculationThing.Balance -> if (byValue) + stats.balance.amount else (stats.incomesCount - stats.expensesCount) + }.toDouble() + } +} \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/action/FormulaFlow.kt b/formula/domain/src/main/java/com/ivy/formula/domain/action/FormulaFlow.kt new file mode 100644 index 0000000000..b944c2b6d4 --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/action/FormulaFlow.kt @@ -0,0 +1,45 @@ +package com.ivy.formula.domain.action + +import arrow.core.NonEmptyList +import com.ivy.common.toNonEmptyList +import com.ivy.core.domain.action.FlowAction +import com.ivy.formula.domain.data.formula.Formula +import com.ivy.formula.domain.data.formula.FormulaInput +import com.ivy.formula.domain.pure.parse.compileFunction +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class FormulaFlow @Inject constructor( + private val dataSourceFlow: DataSourceFlow +) : FlowAction() { + override fun Formula.createFlow(): Flow = executeFormula(this) + + private fun executeFormula(formula: Formula): Flow = + executeFunction( + input = provideInput(formula.input), + function = formula.function + ) + + private fun provideInput( + input: NonEmptyList + ): Flow> { + val inputFlows = input.map { + when (it) { + is FormulaInput.OtherFormula -> executeFormula(it.formula) + is FormulaInput.Source -> dataSourceFlow(it.source) + is FormulaInput.Value -> flowOf(it.value) + } + } + + return combine(inputFlows) { + it.toList().toNonEmptyList() + } + } + + private fun executeFunction( + input: Flow>, + function: String, + ): Flow = compileFunction(function).invoke(input) +} \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/formula/Formula.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/formula/Formula.kt new file mode 100644 index 0000000000..07137414ca --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/formula/Formula.kt @@ -0,0 +1,10 @@ +package com.ivy.formula.domain.data.formula + +import arrow.core.NonEmptyList + +data class Formula( + val id: String, + val displayName: String, + val input: NonEmptyList, + val function: String, +) \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/formula/FormulaInput.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/formula/FormulaInput.kt new file mode 100644 index 0000000000..98566679ca --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/formula/FormulaInput.kt @@ -0,0 +1,9 @@ +package com.ivy.formula.domain.data.formula + +import com.ivy.formula.domain.data.source.DataSource + +sealed interface FormulaInput { + data class Value(val value: Double) : FormulaInput + data class OtherFormula(val formula: Formula) : FormulaInput + data class Source(val source: DataSource) : FormulaInput +} \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/project/Project.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/project/Project.kt new file mode 100644 index 0000000000..a5bc400962 --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/project/Project.kt @@ -0,0 +1,13 @@ +package com.ivy.formula.domain.data.project + +import arrow.core.NonEmptyList +import com.ivy.data.CurrencyCode +import com.ivy.data.time.TimePeriod +import com.ivy.formula.domain.data.formula.Formula + +data class Project( + val info: ProjectInfo, + val formulas: NonEmptyList, + val period: TimePeriod, + val currency: CurrencyCode, +) \ No newline at end of file diff --git a/formula/src/main/java/com/ivy/formula/project/ProjectInfo.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/project/ProjectInfo.kt similarity index 70% rename from formula/src/main/java/com/ivy/formula/project/ProjectInfo.kt rename to formula/domain/src/main/java/com/ivy/formula/domain/data/project/ProjectInfo.kt index 2d0412b637..88faa93bde 100644 --- a/formula/src/main/java/com/ivy/formula/project/ProjectInfo.kt +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/project/ProjectInfo.kt @@ -1,11 +1,11 @@ -package com.ivy.formula.project +package com.ivy.formula.domain.data.project import androidx.compose.ui.graphics.Color import com.ivy.data.ItemIconId data class ProjectInfo( val name: String, + val description: String, val color: Color, val iconId: ItemIconId - // bla bla bla ) \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/project/ProjectView.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/project/ProjectView.kt new file mode 100644 index 0000000000..190ae5d047 --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/project/ProjectView.kt @@ -0,0 +1,8 @@ +package com.ivy.formula.domain.data.project + +import arrow.core.NonEmptyList +import com.ivy.formula.domain.data.formula.Formula + +data class ProjectView( + val formulas: NonEmptyList +) \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/source/Calculation.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/Calculation.kt new file mode 100644 index 0000000000..cddfbd72bb --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/Calculation.kt @@ -0,0 +1,12 @@ +package com.ivy.formula.domain.data.source + +import com.ivy.data.CurrencyCode + +/** + * @param outputCurrency use **null** for base currency + */ +data class Calculation( + val thing: CalculationThing, + val type: CalculationType, + val outputCurrency: CurrencyCode?, +) \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/source/CalculationThing.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/CalculationThing.kt new file mode 100644 index 0000000000..a15ed5027d --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/CalculationThing.kt @@ -0,0 +1,5 @@ +package com.ivy.formula.domain.data.source + +enum class CalculationThing { + Income, Expense, Balance +} \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/source/CalculationType.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/CalculationType.kt new file mode 100644 index 0000000000..3cc81d43fb --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/CalculationType.kt @@ -0,0 +1,5 @@ +package com.ivy.formula.domain.data.source + +enum class CalculationType { + ByValue, ByCount +} \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/data/source/DataSource.kt b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/DataSource.kt new file mode 100644 index 0000000000..c80b318dec --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/data/source/DataSource.kt @@ -0,0 +1,8 @@ +package com.ivy.formula.domain.data.source + +import com.ivy.core.domain.action.transaction.TrnQuery + +data class DataSource( + val filter: TrnQuery, + val calculation: Calculation, +) \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/CompileFunction.kt b/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/CompileFunction.kt new file mode 100644 index 0000000000..3c20b00020 --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/CompileFunction.kt @@ -0,0 +1,26 @@ +package com.ivy.formula.domain.pure.parse + +import arrow.core.Either +import arrow.core.NonEmptyList +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +@OptIn(FlowPreview::class) +fun compileFunction( + function: String +): (Flow>) -> Flow = { argsFlow -> + argsFlow.flatMapLatest { args -> + val parser = FunctionParser(args) + when (val result = parser.parse(normalizeFunction(function))) { + is Either.Left -> error(result.value) + is Either.Right -> flowOf(result.value) + } + } +} + +private fun normalizeFunction(function: String): String = + function.replace("=", "") + .replace(" ", "") + .trim() \ No newline at end of file diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/FunctionParser.kt b/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/FunctionParser.kt new file mode 100644 index 0000000000..4df6c9da48 --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/FunctionParser.kt @@ -0,0 +1,62 @@ +package com.ivy.formula.domain.pure.parse + +import arrow.core.Either +import arrow.core.NonEmptyList +import arrow.core.computations.either +import arrow.core.left + +data class FunctionParser(val args: NonEmptyList) { + + @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") + suspend fun parse(function: String): Either = either { + val result = expr().invoke(function) + if (result.isEmpty()) "Parse error.".left().bind() + if (result.size > 1) "Ambiguous result.".left().bind() + val leftover = result.first().leftover + if (leftover.isNotEmpty()) + "Not completely parsed, leftover: \"$leftover\".".left().bind() + + Either.Right(result.first().value).bind() + } + + private fun expr(): Parser = term().flatMap { x -> + char('+').flatMap { + expr().flatMap { y -> + pure(x + y) + } + } + } or term() + + private fun term(): Parser = factor().flatMap { x -> + char('%').flatMap { + pure(x / 100) + } + } or factor().flatMap { x -> + char('*').flatMap { + term().flatMap { y -> + pure(x * y) + } + } + } or factor().flatMap { x -> + char('/').flatMap { + term().flatMap { y -> + pure(x / y) + } + } + } or factor() + + private fun factor(): Parser = char('(').flatMap { + expr().flatMap { x -> + char(')').flatMap { + pure(x) + } + } + } or argument() + + private fun argument(): Parser = char('$').flatMap { + sat { it.isDigit() }.flatMap { digit -> + val index = digit.digitToInt() - 1 + pure(args[index]) + } + } +} diff --git a/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/Parser.kt b/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/Parser.kt new file mode 100644 index 0000000000..48bef2532b --- /dev/null +++ b/formula/domain/src/main/java/com/ivy/formula/domain/pure/parse/Parser.kt @@ -0,0 +1,105 @@ +package com.ivy.formula.domain.pure.parse + +/** + * Motivated by FUNCTIONAL PEARL + * Monadic parsing in Haskell + * by Graham Hutton & Erik Meijer + */ + +data class ParseResult( + val value: T, + val leftover: String, +) + +typealias Parser = (String) -> List> + +fun pure(value: T): Parser = { text -> + listOf(ParseResult(value, text)) +} + +fun empty(): Parser = { emptyList() } + +fun Parser.flatMap( + f: (T) -> Parser +): Parser = { string -> + val result = this(string) // apply parser 1 + + // apply parser 2 on the result of parser 1 + result.flatMap { + f(it.value).invoke(it.leftover) + } +} + +infix fun Parser.or(parser2: Parser): Parser = { text -> + this(text).takeIf { it.isNotEmpty() } ?: parser2(text) +} + +fun combine(parser1: Parser, parser2: Parser): Parser = { text -> + parser1(text) + parser2(text) +} + +fun combineFirst(parser1: Parser, parser2: Parser): Parser = { text -> + val res = combine(parser1, parser2).invoke(text) + res.take(1) +} + +// region Functions +fun item(): Parser = { string -> + if (string.isNotEmpty()) { + // return the first character as value and the rest as leftover + listOf( + ParseResult( + value = string.first(), + leftover = string.drop(1) + ) + ) + } else emptyList() +} + +fun peek(): Parser = { string -> + if (string.isNotEmpty()) { + listOf(ParseResult(value = string.first(), leftover = string)) + } else emptyList() +} + +/** + * Satisfies a given predicate. + */ +fun sat(predicate: (Char) -> Boolean): Parser = { string -> + item().flatMap { char -> + if (predicate(char)) pure(char) else empty() + }.invoke(string) +} + +fun char(c: Char): Parser = sat { it == c } + +fun charIn(str: String): Parser = sat { str.contains(it) } + +fun string(str: String): Parser = { string -> + if (str.isEmpty()) pure("").invoke(string) else { + // recurse + char(str.first()).flatMap { c -> + string(str.drop(1)).flatMap { cs -> + pure(c + cs) + } + }.invoke(string) + } +} + +fun zeroOrMany(parser: Parser): Parser> { + fun oneOrMany(parser: Parser): Parser> = + parser.flatMap { one -> + zeroOrMany(parser).flatMap { many -> + pure(listOf(one) + many) + } + } + + return combineFirst(oneOrMany(parser), pure(emptyList())) +} + +fun oneOrMany(parser: Parser): Parser> = parser.flatMap { one -> + zeroOrMany(parser).flatMap { many -> + pure(listOf(one) + many) + } +} +// endregion \ No newline at end of file diff --git a/formula/domain/src/test/java/com/ivy/formula/domain/pure/parse/CompileFunctionTest.kt b/formula/domain/src/test/java/com/ivy/formula/domain/pure/parse/CompileFunctionTest.kt new file mode 100644 index 0000000000..b6ab1eec1b --- /dev/null +++ b/formula/domain/src/test/java/com/ivy/formula/domain/pure/parse/CompileFunctionTest.kt @@ -0,0 +1,43 @@ +package com.ivy.formula.domain.pure.parse + +import com.ivy.common.toNonEmptyList +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf + +class CompileFunctionTest : StringSpec({ + fun args(vararg numbers: Double) = flowOf(numbers.toList().toNonEmptyList()) + + "compile just value" { + val f = compileFunction("=$1") + + val res = f(args(20_000.0)) + + res.first() shouldBe 20_000.0 + } + + "compile addition" { + val f = compileFunction("=$1+$2") + + val res = f(args(10.0, 15.0)) + + res.first() shouldBe 25.0 + } + + "compile 10% of 1,000" { + val f = compileFunction("=$1*$2%") + + val res = f(args(1_000.0, 10.0)) + + res.first() shouldBe 100.0 + } + + "compile bracketed expression" { + val f = compileFunction("=($1+$2)/$3") + + val res = f(args(25.0, 35.0, 3.0)) + + res.first() shouldBe 20.0 + } +}) \ No newline at end of file diff --git a/donate/.gitignore b/formula/persistence/.gitignore similarity index 100% rename from donate/.gitignore rename to formula/persistence/.gitignore diff --git a/temp-persistence/build.gradle.kts b/formula/persistence/build.gradle.kts similarity index 58% rename from temp-persistence/build.gradle.kts rename to formula/persistence/build.gradle.kts index f668dfbab7..db94919be3 100644 --- a/temp-persistence/build.gradle.kts +++ b/formula/persistence/build.gradle.kts @@ -1,5 +1,4 @@ import com.ivy.buildsrc.DataStore -import com.ivy.buildsrc.Gson import com.ivy.buildsrc.Hilt import com.ivy.buildsrc.RoomDB @@ -8,14 +7,14 @@ apply() plugins { `android-library` `kotlin-android` - `kotlin-kapt` + `kotlin-kapt` // for Room DB } android { defaultConfig { kapt { arguments { - arg("room.schemaLocation", "$projectDir/schemas") + arg("room.schemaLocation", "$projectDir/../room-db-schemas") } } } @@ -24,8 +23,8 @@ android { dependencies { Hilt() implementation(project(":common:main")) - implementation(project(":core:data-model")) - DataStore(api = true) - RoomDB(api = true) - Gson(api = false) + implementation(project(":core:domain")) + implementation(project(":core:persistence")) + RoomDB(api = false) + DataStore(api = false) } \ No newline at end of file diff --git a/formula/persistence/src/main/AndroidManifest.xml b/formula/persistence/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2440ce2078 --- /dev/null +++ b/formula/persistence/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/IvyWalletFormulaDb.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/IvyWalletFormulaDb.kt new file mode 100644 index 0000000000..759bb0c43f --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/IvyWalletFormulaDb.kt @@ -0,0 +1,46 @@ +package com.ivy.formula.persistence + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.ivy.core.persistence.GeneralTypeConverters +import com.ivy.formula.persistence.dao.DataSourceDao +import com.ivy.formula.persistence.dao.FormulaDao +import com.ivy.formula.persistence.dao.ProjectDao +import com.ivy.formula.persistence.dao.ProjectFormulaLinkDao +import com.ivy.formula.persistence.entity.datasource.DataSourceEntity +import com.ivy.formula.persistence.entity.formula.FormulaEntity +import com.ivy.formula.persistence.entity.project.ProjectEntity +import com.ivy.formula.persistence.entity.project.ProjectFormulaLinkEntity + +@Database( + entities = [ + FormulaEntity::class, ProjectEntity::class, + ProjectFormulaLinkEntity::class, DataSourceEntity::class, + ], + version = 1, + exportSchema = true, +) +@TypeConverters(GeneralTypeConverters::class) +abstract class IvyWalletFormulaDb : RoomDatabase() { + abstract fun formulaDao(): FormulaDao + + abstract fun projectDao(): ProjectDao + + abstract fun projectFormulaLinkDao(): ProjectFormulaLinkDao + + abstract fun dataSourceDao(): DataSourceDao + + + companion object { + private const val DB_NAME = "ivy-wallet-formula.db" + + fun create(applicationContext: Context): IvyWalletFormulaDb { + return Room.databaseBuilder( + applicationContext, IvyWalletFormulaDb::class.java, DB_NAME + ).build() + } + } +} \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/DataSourceDao.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/DataSourceDao.kt new file mode 100644 index 0000000000..80cd58eec2 --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/DataSourceDao.kt @@ -0,0 +1,7 @@ +package com.ivy.formula.persistence.dao + +import androidx.room.Dao + +@Dao +interface DataSourceDao { +} \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/FormulaDao.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/FormulaDao.kt new file mode 100644 index 0000000000..431501f17e --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/FormulaDao.kt @@ -0,0 +1,7 @@ +package com.ivy.formula.persistence.dao + +import androidx.room.Dao + +@Dao +interface FormulaDao { +} \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/ProjectDao.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/ProjectDao.kt new file mode 100644 index 0000000000..893db1a1a2 --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/ProjectDao.kt @@ -0,0 +1,7 @@ +package com.ivy.formula.persistence.dao + +import androidx.room.Dao + +@Dao +interface ProjectDao { +} \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/ProjectFormulaLinkDao.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/ProjectFormulaLinkDao.kt new file mode 100644 index 0000000000..c2c3d1a4bc --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/dao/ProjectFormulaLinkDao.kt @@ -0,0 +1,7 @@ +package com.ivy.formula.persistence.dao + +import androidx.room.Dao + +@Dao +interface ProjectFormulaLinkDao { +} \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/di/FormulaPersistenceModuleDI.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/di/FormulaPersistenceModuleDI.kt new file mode 100644 index 0000000000..01c8dded57 --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/di/FormulaPersistenceModuleDI.kt @@ -0,0 +1,39 @@ +package com.ivy.formula.persistence.di + +import android.content.Context +import com.ivy.formula.persistence.IvyWalletFormulaDb +import com.ivy.formula.persistence.dao.DataSourceDao +import com.ivy.formula.persistence.dao.FormulaDao +import com.ivy.formula.persistence.dao.ProjectDao +import com.ivy.formula.persistence.dao.ProjectFormulaLinkDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + + +@Module +@InstallIn(SingletonComponent::class) +object FormulaPersistenceModuleDI { + @Singleton + @Provides + fun provideIvyWalletFormulaDb( + @ApplicationContext appContext: Context + ): IvyWalletFormulaDb = IvyWalletFormulaDb.create(appContext) + + @Provides + fun provideFormulaDao(db: IvyWalletFormulaDb): FormulaDao = db.formulaDao() + + @Provides + fun provideProjectDao(db: IvyWalletFormulaDb): ProjectDao = db.projectDao() + + @Provides + fun provideProjectFormulaLinkDao( + db: IvyWalletFormulaDb + ): ProjectFormulaLinkDao = db.projectFormulaLinkDao() + + @Provides + fun provideDataSourceDao(db: IvyWalletFormulaDb): DataSourceDao = db.dataSourceDao() +} \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/datasource/DataSourceEntity.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/datasource/DataSourceEntity.kt new file mode 100644 index 0000000000..72a9f2c14c --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/datasource/DataSourceEntity.kt @@ -0,0 +1,13 @@ +package com.ivy.formula.persistence.entity.datasource + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "data_sources") +data class DataSourceEntity( + @PrimaryKey + @ColumnInfo(name = "id", index = true) + val id: String, + // TODO: +) \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/formula/FormulaEntity.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/formula/FormulaEntity.kt new file mode 100644 index 0000000000..984ea7ea22 --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/formula/FormulaEntity.kt @@ -0,0 +1,13 @@ +package com.ivy.formula.persistence.entity.formula + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "formulas") +data class FormulaEntity( + @PrimaryKey + @ColumnInfo(name = "id", index = true) + val id: String, + // TODO: +) \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/project/ProjectEntity.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/project/ProjectEntity.kt new file mode 100644 index 0000000000..477d77a412 --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/project/ProjectEntity.kt @@ -0,0 +1,13 @@ +package com.ivy.formula.persistence.entity.project + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "projects") +data class ProjectEntity( + @PrimaryKey + @ColumnInfo(name = "id", index = true) + val id: String, + // TODO: +) \ No newline at end of file diff --git a/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/project/ProjectFormulaLinkEntity.kt b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/project/ProjectFormulaLinkEntity.kt new file mode 100644 index 0000000000..6d451672ee --- /dev/null +++ b/formula/persistence/src/main/java/com/ivy/formula/persistence/entity/project/ProjectFormulaLinkEntity.kt @@ -0,0 +1,17 @@ +package com.ivy.formula.persistence.entity.project + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity(tableName = "project_formula_links") +data class ProjectFormulaLinkEntity( + @PrimaryKey + @ColumnInfo(name = "id", index = true) + val id: String, + @ColumnInfo(name = "projectId", index = true) + val projectId: String, + @ColumnInfo(name = "formulaId", index = true) + val formulaId: String, +) \ No newline at end of file diff --git a/formula/room-db-schemas/com.ivy.formula.persistence.IvyWalletFormulaDb/1.json b/formula/room-db-schemas/com.ivy.formula.persistence.IvyWalletFormulaDb/1.json new file mode 100644 index 0000000000..93e4cbb7c8 --- /dev/null +++ b/formula/room-db-schemas/com.ivy.formula.persistence.IvyWalletFormulaDb/1.json @@ -0,0 +1,164 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "40cfe45a22ba84df23feb9435b6a5a3b", + "entities": [ + { + "tableName": "formulas", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_formulas_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_formulas_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "projects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_projects_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_projects_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "project_formula_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `projectId` TEXT NOT NULL, `formulaId` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "projectId", + "columnName": "projectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formulaId", + "columnName": "formulaId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_project_formula_links_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_project_formula_links_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_project_formula_links_projectId", + "unique": false, + "columnNames": [ + "projectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_project_formula_links_projectId` ON `${TABLE_NAME}` (`projectId`)" + }, + { + "name": "index_project_formula_links_formulaId", + "unique": false, + "columnNames": [ + "formulaId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_project_formula_links_formulaId` ON `${TABLE_NAME}` (`formulaId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "data_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_data_sources_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_data_sources_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '40cfe45a22ba84df23feb9435b6a5a3b')" + ] + } +} \ No newline at end of file diff --git a/formula/src/main/AndroidManifest.xml b/formula/src/main/AndroidManifest.xml deleted file mode 100644 index a041d6bcfd..0000000000 --- a/formula/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/formula/src/main/java/com/ivy/formula/Formula.kt b/formula/src/main/java/com/ivy/formula/Formula.kt deleted file mode 100644 index 1ea75aae81..0000000000 --- a/formula/src/main/java/com/ivy/formula/Formula.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.ivy.formula - -import arrow.core.NonEmptyList -import arrow.core.nonEmptyListOf -import com.ivy.core.domain.action.transaction.TrnQuery -import com.ivy.core.domain.pure.time.currentMonthlyPeriod -import com.ivy.core.domain.pure.time.range -import com.ivy.formula.source.DataSource -import com.ivy.formula.source.Stat -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf - -typealias Input = Flow - -sealed interface InputItem { - data class F(val formula: Formula) : InputItem - data class Const(val value: Flow) : InputItem -} - -abstract class Formula( - val input: NonEmptyList -) { - abstract fun formula(input: NonEmptyList): Flow - - operator fun invoke(): Flow = formula(input) -} - -fun const(value: Double): Input = flowOf(value) - -fun percent(value: Double): Double = value / 100 - -fun args( - input: NonEmptyList, - f: (args: Array) -> Double -): Flow = combine(*input.toTypedArray()) { f(it) } - -// region Test 1 -suspend fun test() { - /* - input = [const 20k] - " = $1" => "$1" - */ - val f1 = object : Formula( - input = nonEmptyListOf(const(20_000.0)) - ) { - override fun formula(input: NonEmptyList): Flow = args(input) { - it[0] - } - } - - val parseF1 = parse( - input = nonEmptyListOf(InputItem.Const(const(20_000.0))), - formula = "$1" - ) - - /* - input = [f1, const 80] - " = $1 * $2%" => "$1*$2% - */ - val f2 = object : Formula( - input = nonEmptyListOf(f1(), const(80.0)) - ) { - override fun formula(input: NonEmptyList): Flow = args(input) { - it[0] * percent(it[1]) - } - } - - val parseF2 = parse( - input = nonEmptyListOf(InputItem.F(parseF1), InputItem.Const(const(80.0))), - formula = "$1*$2%" - ) -} - -suspend fun parse( - input: NonEmptyList, - formula: String -): Formula { - object : Formula( - input = input.map { - when (it) { - is InputItem.Const -> it.value - is InputItem.F -> it.formula.invoke() - } - } - ) { - override fun formula(input: NonEmptyList): Flow = args(input) { - TODO("Not yet implemented") - } - } - TODO() -} -// endregion - -// region Test 2 -suspend fun test2() { - val cashflowThisMonth = DataSource( - filter = TrnQuery.ActualBetween(currentMonthlyPeriod(startDayOfMonth = 1).range()), - focused = Stat.Balance - ) - - -} -// endregion \ No newline at end of file diff --git a/formula/src/main/java/com/ivy/formula/project/Project.kt b/formula/src/main/java/com/ivy/formula/project/Project.kt deleted file mode 100644 index b888c7a45d..0000000000 --- a/formula/src/main/java/com/ivy/formula/project/Project.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.formula.project - -import arrow.core.NonEmptyList -import com.ivy.formula.Formula - -data class Project( - val info: ProjectInfo, - val formulas: NonEmptyList, - val visualize: NonEmptyList -) \ No newline at end of file diff --git a/formula/src/main/java/com/ivy/formula/source/DataSource.kt b/formula/src/main/java/com/ivy/formula/source/DataSource.kt deleted file mode 100644 index 352117c761..0000000000 --- a/formula/src/main/java/com/ivy/formula/source/DataSource.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.ivy.formula.source - -import com.ivy.core.domain.action.calculate.CalculateFlow -import com.ivy.core.domain.action.transaction.TrnQuery -import com.ivy.core.domain.action.transaction.TrnsFlow -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.map - -enum class Stat { - Balance, Income, Expense, IncomeCount, ExpenseCount, BalanceCount -} - -lateinit var trnsFlow: TrnsFlow -lateinit var calculateFlow: CalculateFlow - - -/* - 1. Filter (Accounts, Categories, ...) - 2. Output currency (default to base) - 3. Stats flow - 4. Select the thing from stats flow - Filter + Output currency => Flow - */ -data class DataSource( - val filter: TrnQuery, - val focused: Stat, -) - -@OptIn(FlowPreview::class) -fun create(dataSource: DataSource): Flow = - trnsFlow(dataSource.filter).flatMapMerge { trns -> - calculateFlow( - CalculateFlow.Input( - trns = trns, - includeTransfers = false, - includeHidden = false - ) - ) - }.map { stats -> - when (dataSource.focused) { - Stat.Balance -> stats.balance.amount - Stat.Income -> stats.income.amount - Stat.Expense -> stats.expense.amount - Stat.IncomeCount -> stats.incomesCount.toDouble() - Stat.ExpenseCount -> stats.expensesCount.toDouble() - Stat.BalanceCount -> (stats.incomesCount - stats.expensesCount).toDouble() - } - } \ No newline at end of file diff --git a/formula/.gitignore b/formula/ui/.gitignore similarity index 100% rename from formula/.gitignore rename to formula/ui/.gitignore diff --git a/formula/ui/build.gradle.kts b/formula/ui/build.gradle.kts new file mode 100644 index 0000000000..e990e014e5 --- /dev/null +++ b/formula/ui/build.gradle.kts @@ -0,0 +1,16 @@ +import com.ivy.buildsrc.Hilt + +apply() + +plugins { + `android-library` + `kotlin-android` +} + +dependencies { + Hilt() + implementation(project(":common:main")) + implementation(project(":core:ui")) + implementation(project(":design-system")) + implementation(project(":formula:domain")) +} \ No newline at end of file diff --git a/formula/ui/src/main/AndroidManifest.xml b/formula/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..fe5cc0be51 --- /dev/null +++ b/formula/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/home/customer-journey/build.gradle.kts b/home/customer-journey/build.gradle.kts index 3aaeaf148b..6d901cec8b 100644 --- a/home/customer-journey/build.gradle.kts +++ b/home/customer-journey/build.gradle.kts @@ -11,13 +11,9 @@ dependencies { Hilt() implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) implementation(project(":core:ui")) implementation(project(":core:data-model")) implementation(project(":navigation")) - implementation(project(":temp-persistence")) - implementation(project(":temp-domain")) implementation(project(":widgets")) } \ No newline at end of file diff --git a/home/customer-journey/src/main/java/com/ivy/journey/CustomerJourney.kt b/home/customer-journey/src/main/java/com/ivy/journey/CustomerJourney.kt deleted file mode 100644 index 4e5473a8f4..0000000000 --- a/home/customer-journey/src/main/java/com/ivy/journey/CustomerJourney.kt +++ /dev/null @@ -1,152 +0,0 @@ -//package com.ivy.journey -// -//import androidx.compose.foundation.background -//import androidx.compose.foundation.clickable -//import androidx.compose.foundation.layout.* -//import androidx.compose.material.Text -//import androidx.compose.runtime.Composable -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.draw.clip -//import androidx.compose.ui.platform.testTag -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import com.ivy.design.l0_system.UI -//import com.ivy.design.l0_system.color.contrastColor -//import com.ivy.design.l0_system.style -//import com.ivy.design.util.ComponentPreview -// -//import com.ivy.journey.domain.CustomerJourneyCardData -//import com.ivy.journey.domain.CustomerJourneyLogic -//import com.ivy.wallet.ui.theme.Gradient -//import com.ivy.wallet.ui.theme.components.IvyButton -//import com.ivy.wallet.ui.theme.components.IvyIcon -//import com.ivy.wallet.ui.theme.dynamicContrast -//import com.ivy.wallet.utils.drawColoredShadow -// -//@Composable -//fun CustomerJourney( -// customerJourneyCards: List, -// onDismiss: (CustomerJourneyCardData) -> Unit -//) { -// -// val rootScreen = com.ivy.core.ui.temp.rootScreen() -// -// if (customerJourneyCards.isNotEmpty()) { -// Spacer(Modifier.height(12.dp)) -// } -// -// for (card in customerJourneyCards) { -// Spacer(Modifier.height(12.dp)) -// -// CustomerJourneyCard( -// cardData = card, -// onDismiss = { -// onDismiss(card) -// } -// ) { -//// card.onAction(nav, ivyContext, rootScreen) -// } -// } -//} -// -//@Composable -//fun CustomerJourneyCard( -// cardData: CustomerJourneyCardData, -// -// onDismiss: () -> Unit, -// onCTA: () -> Unit -//) { -// Column( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 16.dp) -// .drawColoredShadow(cardData.background.start) -// .background(cardData.background.asHorizontalBrush(), UI.shapes.rounded) -// .clip(UI.shapes.rounded) -// .clickable { -// onCTA() -// } -// ) { -// Spacer(Modifier.height(16.dp)) -// -// Row( -// verticalAlignment = Alignment.CenterVertically -// ) { -// Text( -// modifier = Modifier -// .weight(1f) -// .padding(start = 24.dp, end = 16.dp), -// text = cardData.title, -// style = UI.typo.b1.style( -// fontWeight = FontWeight.ExtraBold, -// color = contrastColor(cardData.background.start) -// ) -// ) -// -// if (cardData.hasDismiss) { -// IvyIcon( -// modifier = Modifier -// .clickable { -// onDismiss() -// } -// .padding(8.dp), //enlarge click area -// icon = R.drawable.ic_dismiss, -// tint = cardData.background.start.dynamicContrast(), -// contentDescription = "prompt_dismiss", -// ) -// -// Spacer(Modifier.width(20.dp)) -// } -// } -// -// Spacer(Modifier.height(16.dp)) -// -// Text( -// modifier = Modifier -// .fillMaxWidth() -// .padding(start = 24.dp, end = 32.dp), -// text = cardData.description, -// style = UI.typo.b2.style( -// fontWeight = FontWeight.Medium, -// color = contrastColor(cardData.background.start) -// ) -// ) -// -// Spacer(Modifier.height(32.dp)) -// -// IvyButton( -// modifier = Modifier -// .align(Alignment.End) -// .padding(end = 20.dp) -// .testTag("cta_prompt_${cardData.id}"), -// text = cardData.cta, -// shadowAlpha = 0f, -// iconStart = cardData.ctaIcon, -// iconTint = cardData.background.start, -// textStyle = UI.typo.b2.style( -// color = cardData.background.start, -// fontWeight = FontWeight.Bold -// ), -// padding = 8.dp, -// backgroundGradient = Gradient.solid(contrastColor(cardData.background.start)) -// ) { -// onCTA() -// } -// -// Spacer(Modifier.height(20.dp)) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewCard() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.adjustBalanceCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} \ No newline at end of file diff --git a/home/customer-journey/src/main/java/com/ivy/journey/domain/CustomerJourneyCardData.kt b/home/customer-journey/src/main/java/com/ivy/journey/domain/CustomerJourneyCardData.kt deleted file mode 100644 index e8415d4b77..0000000000 --- a/home/customer-journey/src/main/java/com/ivy/journey/domain/CustomerJourneyCardData.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.ivy.journey.domain - -import androidx.annotation.DrawableRes -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.design.l0_system.color.Gradient - - -data class CustomerJourneyCardData( - val id: String, - val condition: (trnCount: Long, plannedPaymentsCount: Long, ivyContext: IvyWalletCtx) -> Boolean, - - val title: String, - val description: String, - val cta: String, - @DrawableRes val ctaIcon: Int, - - val hasDismiss: Boolean = true, - - val background: Gradient, -// val onAction: (Navigation, IvyWalletCtx, com.ivy.core.ui.temp.RootScreen) -> Unit -) \ No newline at end of file diff --git a/home/customer-journey/src/main/java/com/ivy/journey/domain/CustomerJourneyLogic.kt b/home/customer-journey/src/main/java/com/ivy/journey/domain/CustomerJourneyLogic.kt deleted file mode 100644 index 318cce5a59..0000000000 --- a/home/customer-journey/src/main/java/com/ivy/journey/domain/CustomerJourneyLogic.kt +++ /dev/null @@ -1,438 +0,0 @@ -//package com.ivy.journey.domain -// -//import androidx.compose.runtime.Composable -//import androidx.compose.ui.tooling.preview.Preview -//import com.ivy.common.Constants -//import com.ivy.base.R -//import com.ivy.core.ui.temp.trash.IvyWalletCtx -//import com.ivy.design.l0_system.color.* -//import com.ivy.design.util.ComponentPreview -//import com.ivy.journey.CustomerJourneyCard -//import com.ivy.wallet.io.persistence.SharedPrefs -//import com.ivy.wallet.io.persistence.dao.PlannedPaymentRuleDao -//import com.ivy.wallet.io.persistence.dao.TransactionDao -//import com.ivy.wallet.ui.theme.Ivy -//import com.ivy.widgets.AddTransactionWidgetCompact -// -//@Deprecated("Use FP style, look into `domain.fp` package") -//class CustomerJourneyLogic( -// private val transactionDao: TransactionDao, -// private val plannedPaymentRuleDao: PlannedPaymentRuleDao, -// private val sharedPrefs: SharedPrefs, -// private val ivyContext: IvyWalletCtx -//) { -// -// suspend fun loadCards(): List { -// val trnCount = transactionDao.countHappenedTransactions() -// val plannedPaymentsCount = plannedPaymentRuleDao.countPlannedPayments() -// -// return ACTIVE_CARDS -// .filter { -// it.condition(trnCount, plannedPaymentsCount, ivyContext) && !isCardDismissed(it) -// } -// } -// -// private fun isCardDismissed(cardData: CustomerJourneyCardData): Boolean { -// return sharedPrefs.getBoolean(sharedPrefsKey(cardData), false) -// } -// -// fun dismissCard(cardData: CustomerJourneyCardData) { -// sharedPrefs.putBoolean(sharedPrefsKey(cardData), true) -// } -// -// private fun sharedPrefsKey(cardData: CustomerJourneyCardData): String { -// return "${cardData.id}${SharedPrefs._CARD_DISMISSED}" -// } -// -// companion object { -// val ACTIVE_CARDS = listOf( -// adjustBalanceCard(), -// addPlannedPaymentCard(), -// didYouKnow_pinAddTransactionWidgetCard(), -// addBudgetCard(), -// didYouKnow_expensesPieChart(), -// rateUsCard(), -// shareIvyWalletCard(), -// joinIvyTelegramCard(), -// makeReportCard(), -// rateUsCard_2(), -// shareIvyWalletCard_2(), -// ivyWalletIsOpenSource(), -// donateIvyWallet() -// ) -// -// fun adjustBalanceCard() = CustomerJourneyCardData( -// id = "adjust_balance", -// condition = { trnCount, _, _ -> -// trnCount == 0L -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.adjust_initial_balance), -// description = com.ivy.core.ui.temp.stringRes(R.string.adjust_initial_balance_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.to_accounts), -// ctaIcon = R.drawable.ic_custom_account_s, -// background = Gradient.solid(Ivy), -// hasDismiss = false, -// onAction = { _, ivyContext, _ -> -// ivyContext.selectMainTab(com.ivy.base.MainTab.ACCOUNTS) -// } -// ) -// -// fun addPlannedPaymentCard() = CustomerJourneyCardData( -// id = "add_planned_payment", -// condition = { trnCount, plannedPaymentCount, _ -> -// trnCount >= 1 && plannedPaymentCount == 0L -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.create_first_planned_payment), -// description = com.ivy.core.ui.temp.stringRes(R.string.create_first_planned_payment_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.add_planned_payment), -// ctaIcon = R.drawable.ic_planned_payments, -// background = Gradient.solid(Orange), -// hasDismiss = true, -// onAction = { navigation, _, _ -> -//// navigation.navigateTo( -//// EditPlanned( -//// type = TrnTypeOld.EXPENSE, -//// plannedPaymentRuleId = null -//// ) -//// ) -// } -// ) -// -// fun didYouKnow_pinAddTransactionWidgetCard() = CustomerJourneyCardData( -// id = "add_transaction_widget", -// condition = { trnCount, _, _ -> -// trnCount >= 3 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.did_you_know), -// description = com.ivy.core.ui.temp.stringRes(R.string.widget_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.add_widget), -// ctaIcon = R.drawable.ic_custom_atom_s, -// background = Gradient.solid(GreenLight), -// hasDismiss = true, -// onAction = { _, _, ivyActivity -> -// ivyActivity.pinWidget(AddTransactionWidgetCompact::class.java) -// } -// ) -// -// fun addBudgetCard() = CustomerJourneyCardData( -// id = "add_budget", -// condition = { trnCount, _, _ -> -// trnCount >= 5 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.set_a_budget), -// description = com.ivy.core.ui.temp.stringRes(R.string.set_a_budget_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.add_budget), -// ctaIcon = R.drawable.ic_budget_xs, -// background = Gradient.solid(Green2), -// hasDismiss = true, -// onAction = { navigation, _, _ -> -//// navigation.navigateTo(BudgetScreen) -// } -// ) -// -// fun didYouKnow_expensesPieChart() = CustomerJourneyCardData( -// id = "expenses_pie_chart", -// condition = { trnCount, _, _ -> -// trnCount >= 7 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.did_you_know), -// description = com.ivy.core.ui.temp.stringRes(R.string.expenses_piechart_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.expenses_piechart), -// ctaIcon = R.drawable.ic_custom_bills_s, -// background = Gradient.solid(Red), -// hasDismiss = true, -// onAction = { navigation, _, _ -> -//// navigation.navigateTo(PieChartStatistic(type = TrnTypeOld.EXPENSE)) -// } -// ) -// -// fun rateUsCard() = CustomerJourneyCardData( -// id = "rate_us", -// condition = { trnCount, _, _ -> -// trnCount >= 10 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.review_ivy_wallet), -// description = com.ivy.core.ui.temp.stringRes(R.string.review_ivy_wallet_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.rate_us_on_google_play), -// ctaIcon = R.drawable.ic_custom_star_s, -// background = Gradient.solid(Green), -// hasDismiss = true, -// onAction = { _, _, ivyActivity -> -// ivyActivity.reviewIvyWallet(dismissReviewCard = true) -// } -// ) -// -// fun shareIvyWalletCard() = CustomerJourneyCardData( -// id = "share_ivy_wallet", -// condition = { trnCount, _, _ -> -// trnCount >= 14 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.share_ivy_wallet), -// description = com.ivy.core.ui.temp.stringRes(R.string.help_us_grow), -// cta = com.ivy.core.ui.temp.stringRes(R.string.share_with_friends), -// ctaIcon = R.drawable.ic_custom_family_s, -// background = Gradient.solid(Red3), -// hasDismiss = true, -// onAction = { _, _, ivyActivity -> -// ivyActivity.shareIvyWallet() -// } -// ) -// -// fun joinIvyTelegramCard() = CustomerJourneyCardData( -// id = "join_ivy_telegram", -// condition = { trnCount, _, _ -> -// trnCount >= 16 -// }, -// description = "It looks like that you're enjoying Ivy Wallet! Feel free join our invite-only Ivy Telegram Community and make our app better :)", -// title = "Ivy Community", -// cta = "Join now", -// ctaIcon = R.drawable.ic_telegram_24dp, -// background = Gradient.solid(Blue), -// hasDismiss = true, -// onAction = { _, _, rootActivity -> -// rootActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE) -// } -// ) -// -// fun makeReportCard() = CustomerJourneyCardData( -// id = "make_report", -// condition = { trnCount, _, _ -> -// trnCount >= 18 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.did_you_know), -// description = com.ivy.core.ui.temp.stringRes(R.string.make_a_report_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.make_a_report), -// ctaIcon = R.drawable.ic_statistics_xs, -// background = Gradient.solid(Green2), -// hasDismiss = true, -// onAction = { navigation, _, _ -> -//// navigation.navigateTo(Report) -// } -// ) -// -// fun rateUsCard_2() = CustomerJourneyCardData( -// id = "rate_us_2", -// condition = { trnCount, _, _ -> -// trnCount >= 22 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.review_ivy_wallet), -// description = com.ivy.core.ui.temp.stringRes(R.string.make_ivy_wallet_better_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.rate_us_on_google_play), -// ctaIcon = R.drawable.ic_custom_star_s, -// background = Gradient.solid(GreenLight), -// hasDismiss = true, -// onAction = { _, _, ivyActivity -> -// ivyActivity.reviewIvyWallet(dismissReviewCard = true) -// } -// ) -// -// fun shareIvyWalletCard_2() = CustomerJourneyCardData( -// id = "share_ivy_wallet_2", -// condition = { trnCount, _, _ -> -// trnCount >= 24 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.we_need_your_help), -// description = com.ivy.core.ui.temp.stringRes(R.string.we_need_your_help_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.share_ivy_wallet), -// ctaIcon = R.drawable.ic_custom_family_s, -// background = Gradient.solid(Purple2), -// hasDismiss = true, -// onAction = { _, _, ivyActivity -> -// ivyActivity.shareIvyWallet() -// } -// ) -// -// fun ivyWalletIsOpenSource() = CustomerJourneyCardData( -// id = "open_source", -// condition = { trnCount, _, _ -> -// trnCount >= 28 -// }, -// title = com.ivy.core.ui.temp.stringRes(R.string.ivy_wallet_is_opensource), -// description = com.ivy.core.ui.temp.stringRes(R.string.ivy_wallet_is_opensource_description), -// cta = com.ivy.core.ui.temp.stringRes(R.string.contribute), -// ctaIcon = R.drawable.github_logo, -// background = Gradient.solid(Blue3), -// hasDismiss = true, -// onAction = { _, _, ivyActivity -> -// ivyActivity.openUrlInBrowser(Constants.URL_IVY_WALLET_REPO) -// } -// ) -// -// fun donateIvyWallet() = CustomerJourneyCardData( -// id = "donate_ivy_wallet", -// condition = { trnCount, _, _ -> -// trnCount >= 30 -// }, -// title = "Support Ivy Wallet", -// description = "It seems like you enjoy free and open-source software. We too! " + -// "That's why we opened a donations channel to sustain and improve our small project.", -// cta = "Donate", -// ctaIcon = R.drawable.ic_donate_crown, -// background = SunsetNight, -// hasDismiss = true, -// onAction = { nav, _, _ -> -//// nav.navigateTo(DonateScreen) -// } -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewAdjustBalanceCard() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.adjustBalanceCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewAddPlannedPaymentCard() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.addPlannedPaymentCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewDidYouKnow_PinAddTransactionWidgetCard() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.didYouKnow_pinAddTransactionWidgetCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewAddBudgetCard() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.addBudgetCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewDidYouKnow_ExpensesPieChart() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.didYouKnow_expensesPieChart(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewRateUsCard() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.rateUsCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewShareIvyWallet() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.shareIvyWalletCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewJoinTelegram() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.joinIvyTelegramCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewMakeReport() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.makeReportCard(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewRateUs_2() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.rateUsCard_2(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewShaveIvyWallet_2() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.shareIvyWalletCard_2(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewIvyWallet_isOpenSource() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.ivyWalletIsOpenSource(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -//@Preview -//@Composable -//private fun PreviewDonateCard() { -// ComponentPreview { -// CustomerJourneyCard( -// cardData = CustomerJourneyLogic.donateIvyWallet(), -// onCTA = { }, -// onDismiss = {} -// ) -// } -//} -// -// -// -// diff --git a/home/more-menu/build.gradle.kts b/home/more-menu/build.gradle.kts index 5a31a42dbd..491ea506c8 100644 --- a/home/more-menu/build.gradle.kts +++ b/home/more-menu/build.gradle.kts @@ -10,9 +10,8 @@ dependencies { Hilt() implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) implementation(project(":core:ui")) + implementation(project(":core:domain")) implementation(project(":core:data-model")) implementation(project(":navigation")) } \ No newline at end of file diff --git a/home/more-menu/src/main/java/com/ivy/menu/HomeMoreMenu.kt b/home/more-menu/src/main/java/com/ivy/menu/HomeMoreMenu.kt index d79cc1788b..5c493eaab4 100644 --- a/home/more-menu/src/main/java/com/ivy/menu/HomeMoreMenu.kt +++ b/home/more-menu/src/main/java/com/ivy/menu/HomeMoreMenu.kt @@ -1,608 +1,153 @@ package com.ivy.menu - -import androidx.annotation.DrawableRes -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import com.ivy.common.Constants +import com.ivy.core.ui.uiStatePreviewSafe import com.ivy.data.Theme import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.SunsetNight -import com.ivy.design.l0_system.style +import com.ivy.design.l1_buildingBlocks.ColumnRoot +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l1_buildingBlocks.SpacerWeight +import com.ivy.design.l2_components.modal.CloseButton +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.ui.theme.Blue -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.BufferBattery -import com.ivy.wallet.ui.theme.components.CircleButtonFilled -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.modal.AddModalBackHandling -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 -import com.ivy.wallet.utils.* -import java.util.* -import kotlin.math.roundToInt - -private const val SWIPE_UP_THRESHOLD_CLOSE_MORE_MENU = 300 - +import com.ivy.design.util.hiltViewModelPreviewSafe @Composable -fun BoxWithConstraintsScope.MoreMenu( - expanded: Boolean, - - balance: Double, - buffer: Double, - currency: String, - theme: Theme, - - setExpanded: (Boolean) -> Unit, - onSwitchTheme: () -> Unit, - onBufferClick: () -> Unit, - onCurrencyClick: () -> Unit +fun HomeMoreMenu( + visible: Boolean, + onMenuClose: () -> Unit ) { -// val ivyContext = com.ivy.core.ui.temp.ivyWalletCtx() - - val percentExpanded by animateFloatAsState( - targetValue = if (expanded) 1f else 0f, - animationSpec = springBounce() - ) - val iconRotation by animateFloatAsState( - targetValue = if (expanded) -180f else 0f, - animationSpec = springBounce() - ) - - val buttonSizePx = 40.dp.toDensityPx() - - val screenWidth = LocalConfiguration.current.screenWidthDp.dp.toDensityPx() - val screenHeight = LocalConfiguration.current.screenHeightDp.dp.toDensityPx() - val xBase = screenWidth - 24.dp.toDensityPx() - val yBaseCollapsed = 20.dp.toDensityPx() + statusBarInset() - val yBaseExpanded = screenHeight - 48.dp.toDensityPx() - navigationBarInset() - - val yButton = lerp( - start = yBaseCollapsed, - end = yBaseExpanded - buttonSizePx, - fraction = percentExpanded - ) - - //Background - val colorMedium = UI.colors.medium - if (percentExpanded > 0.01f) { - Canvas( - modifier = Modifier - .fillMaxSize() - .clickableNoIndication { - //do nothing - } - .zIndex(500f) - ) { - val radiusCollapsed = buttonSizePx / 2f - val radiusExpanded = screenHeight * 1.5f - val radius = lerp(radiusCollapsed, radiusExpanded, percentExpanded) - - val yBackground = lerp( - start = yBaseCollapsed + radius, - end = yBaseExpanded, - fraction = percentExpanded - ) - - drawCircle( - color = colorMedium, - center = Offset( - x = xBase - buttonSizePx / 2f, - y = yBackground - ), - radius = radius - ) + val viewModel: HomeMoreMenuViewModel? = hiltViewModelPreviewSafe() + val state = uiStatePreviewSafe(viewModel = viewModel, preview = ::previewState) + + AnimatedVisibility( + modifier = Modifier.zIndex(10_000f), + visible = visible, + enter = slideInVertically { -it }, + exit = slideOutVertically { -it }, + ) { + BackHandler(enabled = visible) { + onMenuClose() } - } - if (percentExpanded > 0.01f) { - Column( + ColumnRoot( modifier = Modifier - .statusBarsPadding() - .navigationBarsPadding() - .fillMaxSize() - .alpha(percentExpanded) - .verticalScroll(rememberScrollState()) - .zIndex(510f) - .verticalSwipeListener( - sensitivity = SWIPE_UP_THRESHOLD_CLOSE_MORE_MENU, - onSwipeUp = { - setExpanded(false) - } - ) + .background(UI.colors.pure) ) { - val modalId = remember { - UUID.randomUUID() + SpacerWeight(weight = 1f) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "Categories", + icon = null + ) { + viewModel?.onEvent(MoreMenuEvent.CategoriesClick) } - - AddModalBackHandling( - modalId = modalId, - visible = expanded + SpacerVer(height = 16.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "Settings", + icon = null ) { - setExpanded(false) + viewModel?.onEvent(MoreMenuEvent.SettingsClick) } - - Content( - theme = theme, - onSwitchTheme = onSwitchTheme, - balance = balance, - buffer = buffer, - currency = currency, - onBufferClick = onBufferClick, - onCurrencyClick = onCurrencyClick - ) - } - } - - if (percentExpanded > 0.01f) { - DonateButton( - percentExpanded = percentExpanded - ) - } - - CircleButtonFilled( - modifier = Modifier - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - layout(placeable.width, placeable.height) { - placeable.place( - x = xBase.roundToInt() - buttonSizePx.roundToInt(), - y = yButton.roundToInt() - ) + SpacerVer(height = 16.dp) + ToggleTheme( + theme = state.theme, + onThemeChange = { + viewModel?.onEvent(MoreMenuEvent.ThemeChange(it)) } - } - .rotate(iconRotation) - .thenIf(expanded) { - zIndex(520f) - } - .testTag("home_more_menu_arrow"), - backgroundColor = colorLerp(UI.colors.medium, UI.colors.pure, percentExpanded), - icon = R.drawable.ic_expandarrow - ) { - setExpanded(!expanded) - } -} - -@Composable -private fun ColumnScope.Content( - balance: Double, - buffer: Double, - currency: String, - theme: Theme, - - onSwitchTheme: () -> Unit, - onBufferClick: () -> Unit, - onCurrencyClick: () -> Unit, -) { - Spacer(Modifier.height(24.dp)) - - - SearchButton { -// nav.navigateTo( -// screen = Search -// ) - } - - Spacer(Modifier.height(16.dp)) - - QuickAccess( - theme = theme, - onSwitchTheme = onSwitchTheme - ) - - Spacer(Modifier.height(40.dp)) - - Buffer( - buffer = buffer, - currency = currency, - balance = balance, - onBufferClick = onBufferClick - ) - - Spacer(Modifier.height(16.dp)) - - OpenSource() - - Spacer(Modifier.weight(1f)) -} - -@Composable -private fun SearchButton( - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.fullyRounded) - .background(UI.colors.pure) - .border(1.dp, Gray, UI.shapes.fullyRounded) - .clickable { - onClick() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - IvyIcon(icon = R.drawable.ic_search) - - Spacer(Modifier.width(12.dp)) - - Text( - modifier = Modifier.padding( - vertical = 12.dp, - ), - text = stringResource(R.string.search_transactions), - style = UI.typo.b2.style( - fontWeight = FontWeight.SemiBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.width(16.dp)) - } -} - -@Composable -private fun ColumnScope.OpenSource() { - val uriHandler = LocalUriHandler.current - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .background(UI.colors.pure) - .clickable { - openUrl( - uriHandler = uriHandler, - url = Constants.URL_IVY_WALLET_REPO - ) - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon( - icon = R.drawable.github_logo - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 24.dp) - ) { - Text( - text = stringResource(R.string.ivy_wallet_open_source), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold - ) ) - - Spacer(Modifier.height(4.dp)) - - Text( - text = Constants.URL_IVY_WALLET_REPO, - style = UI.typo.c.style( - fontWeight = FontWeight.ExtraBold, - color = Blue - ) + SpacerWeight(weight = 1f) + CloseButton( + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = onMenuClose ) + SpacerVer(height = 48.dp) } - } } @Composable -private fun ColumnScope.Buffer( - buffer: Double, - currency: String, - balance: Double, - onBufferClick: () -> Unit +private fun ToggleTheme( + theme: Theme, + onThemeChange: (Theme) -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() - .clickableNoIndication { - onBufferClick() - } - .testTag("savings_goal_row"), + .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(24.dp)) - - Text( - text = stringResource(R.string.savings_goal), - style = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.weight(1f)) - - AmountCurrencyB1( - amount = buffer, - currency = currency, - amountFontWeight = FontWeight.ExtraBold - ) - - Spacer(Modifier.width(32.dp)) - } - - Spacer(Modifier.height(12.dp)) - - BufferBattery( - modifier = Modifier.padding(horizontal = 16.dp), - buffer = buffer, - currency = currency, - balance = balance, - ) { - onBufferClick() - } -} - -@Composable -private fun QuickAccess( - theme: Theme, - onSwitchTheme: () -> Unit -) { - - - Text( - modifier = Modifier.padding(start = 24.dp), - text = stringResource(R.string.quick_access) - ) - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.Top - ) { - Spacer(Modifier.weight(1f)) - - MoreMenuButton( - icon = R.drawable.home_more_menu_settings, - label = stringResource(R.string.settings) - ) { - //TODO: Fix that -// nav.navigateTo(SettingsScreen(versionName = "WIP", versionCode = "???")) - } - - Spacer(Modifier.weight(1f)) - - MoreMenuButton( - icon = R.drawable.home_more_menu_categories, - label = stringResource(R.string.categories) - ) { -// nav.navigateTo(Categories) - } - - Spacer(Modifier.weight(1f)) - - MoreMenuButton( - icon = when (theme) { - Theme.LIGHT -> R.drawable.home_more_menu_light_mode - Theme.DARK -> R.drawable.home_more_menu_dark_mode - Theme.AUTO -> R.drawable.home_more_menu_auto_mode - }, - label = when (theme) { - Theme.LIGHT -> stringResource(R.string.light_mode) - Theme.DARK -> stringResource(R.string.dark_mode) - Theme.AUTO -> stringResource(R.string.auto_mode) - }, - backgroundColor = when (theme) { - Theme.LIGHT -> UI.colors.pure - Theme.DARK -> UI.colorsInverted.pure - Theme.AUTO -> UI.colors.pure - }, - tint = when (theme) { - Theme.LIGHT -> UI.colorsInverted.pure - Theme.DARK -> UI.colors.pure - Theme.AUTO -> UI.colorsInverted.pure - } - ) { - onSwitchTheme() - } - - Spacer(Modifier.weight(1f)) - - MoreMenuButton( - icon = R.drawable.home_more_menu_planned_payments, - label = stringResource(R.string.planned_payments) - ) { -// nav.navigateTo(PlannedPayments) - } - - Spacer(Modifier.weight(1f)) - } - - Spacer(Modifier.height(16.dp)) - - //Second Row - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.Top - ) { - Spacer(Modifier.weight(1f)) - - val context = LocalContext.current -// MoreMenuButton( -// icon = R.drawable.home_more_menu_reports, -// label = "Charts" -// ) { -// ivyContext.navigateTo(Screen.Charts) -// } - - val rootScreen = com.ivy.core.ui.temp.rootScreen() - MoreMenuButton( - icon = R.drawable.home_more_menu_share, - label = stringResource(R.string.share_ivy) + IvyButton( + modifier = Modifier.weight(1f), + size = ButtonSize.Big, + visibility = if (theme == Theme.Light) Visibility.High else Visibility.Medium, + feeling = Feeling.Positive, + text = "Light", + icon = null, ) { - rootScreen.shareIvyWallet() + onThemeChange(Theme.Light) } - - Spacer(Modifier.weight(1f)) - - MoreMenuButton( - icon = R.drawable.home_more_menu_reports, - label = stringResource(R.string.reports), + SpacerHor(width = 12.dp) + IvyButton( + modifier = Modifier.weight(1f), + size = ButtonSize.Big, + visibility = if (theme == Theme.Dark) Visibility.High else Visibility.Medium, + feeling = Feeling.Positive, + text = "Dark", + icon = null, ) { -// nav.navigateTo(Report) + onThemeChange(Theme.Dark) } - - Spacer(Modifier.weight(1f)) - - MoreMenuButton( - icon = R.drawable.home_more_menu_budgets, - label = stringResource(R.string.budgets), + SpacerHor(width = 12.dp) + IvyButton( + modifier = Modifier.weight(1f), + size = ButtonSize.Big, + visibility = if (theme == Theme.Auto) Visibility.High else Visibility.Medium, + feeling = Feeling.Positive, + text = "Auto", + icon = null, ) { -// nav.navigateTo(BudgetScreen) + onThemeChange(Theme.Auto) } - - Spacer(Modifier.weight(1f)) - - MoreMenuButton( - icon = R.drawable.home_more_menu_loans, - label = stringResource(R.string.loans), - ) { -// nav.navigateTo(Loans) - } - - Spacer(Modifier.weight(1f)) } } -@Composable -private fun MoreMenuButton( - @DrawableRes icon: Int, - label: String, - - backgroundColor: Color = UI.colors.pure, - tint: Color = UI.colorsInverted.pure, - expandPadding: Dp = 14.dp, - - onClick: () -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircleButtonFilled( - icon = icon, - backgroundColor = backgroundColor, - tint = tint, - clickAreaPadding = expandPadding, - onClick = onClick - ) - - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier - .defaultMinSize(minWidth = 92.dp) - .clickableNoIndication { - onClick() - }, - text = label, - style = UI.typo.c.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold, - textAlign = TextAlign.Center - ) - ) - } -} @Preview @Composable -private fun Preview_Expanded() { +private fun HomeMoreMenuPreview() { IvyPreview { - MoreMenu( - expanded = true, - balance = 7523.43, - buffer = 5000.0, - currency = "BGN", - theme = Theme.LIGHT, - setExpanded = { - }, - onSwitchTheme = { }, - onBufferClick = { } - ) { - - } - } -} - -@Composable -private fun BoxWithConstraintsScope.DonateButton( - percentExpanded: Float -) { - - IvyButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .navigationBarsPadding() - .padding(bottom = 40.dp) - .zIndex(510f) - .alpha(percentExpanded), - text = "Donate", - iconStart = R.drawable.ic_donate_crown, - iconEdgePadding = 16.dp, - iconTextPadding = 12.dp, - backgroundGradient = Gradient.from(SunsetNight) - ) { -// nav.navigateTo(DonateScreen) + HomeMoreMenu( + visible = true, + onMenuClose = {} + ) } } -@Preview -@Composable -private fun Preview() { - IvyPreview { - var expanded by remember { mutableStateOf(false) } - - MoreMenu( - expanded = expanded, - balance = 7523.43, - buffer = 5000.0, - currency = "BGN", - theme = Theme.LIGHT, - setExpanded = { - expanded = it - }, - onSwitchTheme = { }, - onBufferClick = { } - ) { - - } - } -} \ No newline at end of file +private fun previewState() = MoreMenuState( + theme = Theme.Auto, +) diff --git a/home/more-menu/src/main/java/com/ivy/menu/HomeMoreMenuViewModel.kt b/home/more-menu/src/main/java/com/ivy/menu/HomeMoreMenuViewModel.kt new file mode 100644 index 0000000000..4f5f249c15 --- /dev/null +++ b/home/more-menu/src/main/java/com/ivy/menu/HomeMoreMenuViewModel.kt @@ -0,0 +1,50 @@ +package com.ivy.menu + +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.settings.theme.ThemeFlow +import com.ivy.core.domain.action.settings.theme.WriteThemeAct +import com.ivy.data.Theme +import com.ivy.navigation.Navigator +import com.ivy.navigation.destinations.Destination +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class HomeMoreMenuViewModel @Inject constructor( + private val navigator: Navigator, + private val themeFlow: ThemeFlow, + private val writeThemeAct: WriteThemeAct, +) : SimpleFlowViewModel() { + override val initialUi = MoreMenuState( + theme = Theme.Auto + ) + + override val uiFlow: Flow = themeFlow(Unit).map { theme -> + MoreMenuState( + theme = theme + ) + } + + + // region Event Handling + override suspend fun handleEvent(event: MoreMenuEvent) = when (event) { + MoreMenuEvent.CategoriesClick -> handleCategoriesClick() + MoreMenuEvent.SettingsClick -> handleSettingsClick() + is MoreMenuEvent.ThemeChange -> handleThemeChange(event) + } + + private fun handleCategoriesClick() { + navigator.navigate(Destination.categories.destination(Unit)) + } + + private fun handleSettingsClick() { + navigator.navigate(Destination.settings.destination(Unit)) + } + + private suspend fun handleThemeChange(event: MoreMenuEvent.ThemeChange) { + writeThemeAct(event.theme) + } + // endregion +} \ No newline at end of file diff --git a/home/more-menu/src/main/java/com/ivy/menu/MoreMenuEvent.kt b/home/more-menu/src/main/java/com/ivy/menu/MoreMenuEvent.kt new file mode 100644 index 0000000000..16c3ee425e --- /dev/null +++ b/home/more-menu/src/main/java/com/ivy/menu/MoreMenuEvent.kt @@ -0,0 +1,9 @@ +package com.ivy.menu + +import com.ivy.data.Theme + +sealed interface MoreMenuEvent { + object CategoriesClick : MoreMenuEvent + object SettingsClick : MoreMenuEvent + data class ThemeChange(val theme: Theme) : MoreMenuEvent +} \ No newline at end of file diff --git a/home/more-menu/src/main/java/com/ivy/menu/MoreMenuState.kt b/home/more-menu/src/main/java/com/ivy/menu/MoreMenuState.kt new file mode 100644 index 0000000000..481cac99fc --- /dev/null +++ b/home/more-menu/src/main/java/com/ivy/menu/MoreMenuState.kt @@ -0,0 +1,9 @@ +package com.ivy.menu + +import androidx.compose.runtime.Immutable +import com.ivy.data.Theme + +@Immutable +data class MoreMenuState( + val theme: Theme, +) \ No newline at end of file diff --git a/home/tab/build.gradle.kts b/home/tab/build.gradle.kts index 9f0d26af3d..6a8d7b5a14 100644 --- a/home/tab/build.gradle.kts +++ b/home/tab/build.gradle.kts @@ -10,16 +10,13 @@ dependencies { Hilt() implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) implementation(project(":core:ui")) implementation(project(":core:data-model")) implementation(project(":navigation")) - implementation(project(":temp-domain")) implementation(project(":core:domain")) implementation(project(":core:persistence")) + implementation(project(":main:base")) implementation(project(":home:more-menu")) implementation(project(":home:customer-journey")) -// implementation(project(":pie-charts")) } \ No newline at end of file diff --git a/home/tab/src/main/java/com/ivy/home/HomeEvent.kt b/home/tab/src/main/java/com/ivy/home/HomeEvent.kt new file mode 100644 index 0000000000..2200c5fb84 --- /dev/null +++ b/home/tab/src/main/java/com/ivy/home/HomeEvent.kt @@ -0,0 +1,19 @@ +package com.ivy.home + +import com.ivy.main.base.MainBottomBarAction + +sealed interface HomeEvent { + object BalanceClick : HomeEvent + object IncomeClick : HomeEvent + object ExpenseClick : HomeEvent + object HiddenBalanceClick : HomeEvent + object MoreClick : HomeEvent + + data class BottomBarAction(val action: MainBottomBarAction) : HomeEvent + object ShowBottomBar : HomeEvent + object HideBottomBar : HomeEvent + + object AddTransfer : HomeEvent + object AddIncome : HomeEvent + object AddExpense : HomeEvent +} \ No newline at end of file diff --git a/home/tab/src/main/java/com/ivy/home/HomeTab.kt b/home/tab/src/main/java/com/ivy/home/HomeTab.kt index 419e4f7d9a..6520da4529 100644 --- a/home/tab/src/main/java/com/ivy/home/HomeTab.kt +++ b/home/tab/src/main/java/com/ivy/home/HomeTab.kt @@ -27,17 +27,14 @@ import com.ivy.design.l1_buildingBlocks.SpacerVer import com.ivy.design.l1_buildingBlocks.SpacerWeight import com.ivy.design.l2_components.modal.IvyModal import com.ivy.design.l2_components.modal.rememberIvyModal -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling -import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility -import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.IvyPreview import com.ivy.home.components.Balance import com.ivy.home.components.BalanceMini import com.ivy.home.components.IncomeExpense import com.ivy.home.components.MoreMenuButton -import com.ivy.home.event.HomeEvent +import com.ivy.home.modal.AddTransactionModal import com.ivy.home.state.HomeStateUi +import com.ivy.menu.HomeMoreMenu import kotlinx.coroutines.launch @Composable @@ -73,6 +70,7 @@ private fun BoxScope.UI( onBalanceClick = { onEvent(HomeEvent.BalanceClick) }, onIncomeClick = { onEvent(HomeEvent.IncomeClick) }, onExpenseClick = { onEvent(HomeEvent.ExpenseClick) }, + onMoreClick = { onEvent(HomeEvent.MoreClick) } ) }, contentBelowTrns = { @@ -80,12 +78,28 @@ private fun BoxScope.UI( // TODO: Change that to 300.dp when we have transactions SpacerVer(height = 600.dp) } + }, + onFirstVisibleItemChange = { firstVisibleItemIndex -> + if (firstVisibleItemIndex > 0) { + onEvent(HomeEvent.HideBottomBar) + } else { + onEvent(HomeEvent.ShowBottomBar) + } + } + ) + + HomeMoreMenu( + visible = state.moreMenuVisible, + onMenuClose = { + onEvent(HomeEvent.MoreClick) } ) Modals( periodModal = periodModal, - selectedPeriod = state.period + selectedPeriod = state.period, + addTransactionModal = state.addTransactionModal, + onEvent = onEvent, ) } @@ -98,6 +112,7 @@ fun LazyListScope.header( expense: ValueUi, listState: LazyListState, onBalanceClick: () -> Unit, + onMoreClick: () -> Unit, onIncomeClick: () -> Unit, onExpenseClick: () -> Unit, ) { @@ -106,7 +121,8 @@ fun LazyListScope.header( periodModal = periodModal, balance = balance, listState = listState, - onBalanceClick = onBalanceClick + onBalanceClick = onBalanceClick, + onMoreClick = onMoreClick, ) item(key = "home_header_balance") { SpacerVer(height = 4.dp) @@ -121,7 +137,7 @@ fun LazyListScope.header( onExpenseClick = onExpenseClick, ) } - item { + item(key = "header_divider_line") { SpacerVer(height = 24.dp) DividerHor() } @@ -133,7 +149,8 @@ private fun LazyListScope.toolbar( periodModal: IvyModal, balance: ValueUi, listState: LazyListState, - onBalanceClick: () -> Unit + onBalanceClick: () -> Unit, + onMoreClick: () -> Unit = {}, ) { stickyHeader( key = "home_tab_toolbar", @@ -154,9 +171,7 @@ private fun LazyListScope.toolbar( ) } SpacerWeight(weight = 1f) - MoreMenuButton { - // TODO: Implement - } + MoreMenuButton(onClick = onMoreClick) } val headerCollapsed by remember { @@ -188,23 +203,10 @@ private fun CollapsedToolbarExtension( onScrollToTop: () -> Unit ) { Column { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - BalanceMini( - balance = balance, - onClick = onBalanceClick - ) - SpacerWeight(weight = 1f) - IvyButton( - size = ButtonSize.Small, - visibility = ButtonVisibility.Low, - feeling = ButtonFeeling.Positive, - text = "Scroll to top", - icon = null, - onClick = onScrollToTop, - ) - } + BalanceMini( + balance = balance, + onClick = onBalanceClick + ) SpacerVer(height = 4.dp) DividerHor(size = DividerSize.FillMax(padding = 0.dp)) } @@ -215,7 +217,9 @@ private fun CollapsedToolbarExtension( @Composable private fun BoxScope.Modals( periodModal: IvyModal, - selectedPeriod: SelectedPeriodUi? + selectedPeriod: SelectedPeriodUi?, + addTransactionModal: IvyModal, + onEvent: (HomeEvent) -> Unit, ) { if (selectedPeriod != null) { PeriodModal( @@ -223,6 +227,19 @@ private fun BoxScope.Modals( selectedPeriod = selectedPeriod ) } + + AddTransactionModal( + modal = addTransactionModal, + onAddTransfer = { + onEvent(HomeEvent.AddTransfer) + }, + onAddIncome = { + onEvent(HomeEvent.AddIncome) + }, + onAddExpense = { + onEvent(HomeEvent.AddExpense) + } + ) } // endregion @@ -243,7 +260,9 @@ private fun Preview() { income = ValueUi("1,500.35", "USD"), expense = ValueUi("3,000.50", "USD"), hideBalance = false, - trnsList = sampleTransactionListUi() + moreMenuVisible = false, + trnsList = sampleTransactionListUi(), + addTransactionModal = rememberIvyModal() ), onEvent = {} ) diff --git a/home/tab/src/main/java/com/ivy/home/HomeViewModel.kt b/home/tab/src/main/java/com/ivy/home/HomeViewModel.kt index cb4bace5a8..d4c8ff1aa4 100644 --- a/home/tab/src/main/java/com/ivy/home/HomeViewModel.kt +++ b/home/tab/src/main/java/com/ivy/home/HomeViewModel.kt @@ -1,38 +1,41 @@ package com.ivy.home +import com.ivy.common.time.beginningOfIvyTime import com.ivy.core.domain.FlowViewModel import com.ivy.core.domain.action.calculate.CalculateFlow import com.ivy.core.domain.action.calculate.wallet.TotalBalanceFlow import com.ivy.core.domain.action.helper.TrnsListFlow import com.ivy.core.domain.action.period.SelectedPeriodFlow -import com.ivy.core.domain.action.settings.balance.HideBalanceSettingFlow +import com.ivy.core.domain.action.settings.balance.HideBalanceFlow import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow -import com.ivy.core.domain.action.transaction.TrnQuery.ActualBetween -import com.ivy.core.domain.action.transaction.TrnQuery.DueBetween -import com.ivy.core.domain.action.transaction.or +import com.ivy.core.domain.action.transaction.* +import com.ivy.core.domain.action.transaction.TrnQuery.* import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.domain.pure.format.format import com.ivy.core.domain.pure.time.range import com.ivy.core.ui.action.mapping.MapSelectedPeriodUiAct -import com.ivy.core.ui.action.mapping.MapTransactionListUiAct +import com.ivy.core.ui.action.mapping.trn.MapTransactionListUiAct import com.ivy.core.ui.data.transaction.TransactionsListUi import com.ivy.data.Value import com.ivy.data.time.SelectedPeriod +import com.ivy.data.time.TimeRange +import com.ivy.data.transaction.TransactionType import com.ivy.data.transaction.TransactionsList -import com.ivy.data.transaction.TrnListItem -import com.ivy.home.event.HomeBottomBarAction -import com.ivy.home.event.HomeEvent +import com.ivy.data.transaction.TrnPurpose +import com.ivy.design.l2_components.modal.IvyModal import com.ivy.home.state.HomeState import com.ivy.home.state.HomeStateUi +import com.ivy.main.base.MainBottomBarAction +import com.ivy.main.base.MainBottomBarVisibility import com.ivy.navigation.Navigator import com.ivy.navigation.destinations.Destination +import com.ivy.navigation.destinations.transaction.NewTransaction import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import javax.inject.Inject -@OptIn(FlowPreview::class) @HiltViewModel class HomeViewModel @Inject constructor( private val balanceFlow: TotalBalanceFlow, @@ -40,13 +43,15 @@ class HomeViewModel @Inject constructor( private val trnsListFlow: TrnsListFlow, private val baseCurrencyFlow: BaseCurrencyFlow, private val calculateFlow: CalculateFlow, - private val hideBalanceSettingFlow: HideBalanceSettingFlow, + private val hideBalanceFlow: HideBalanceFlow, private val mapSelectedPeriodUiAct: MapSelectedPeriodUiAct, private val mapTransactionListUiAct: MapTransactionListUiAct, private val navigator: Navigator, + private val mainBottomBarVisibility: MainBottomBarVisibility, + private val trnsFlow: TrnsFlow, ) : FlowViewModel() { // region Initial state - override fun initialState(): HomeState = HomeState( + override val initialState: HomeState = HomeState( period = null, trnsList = TransactionsList( upcoming = null, @@ -59,7 +64,9 @@ class HomeViewModel @Inject constructor( hideBalance = false, ) - override fun initialUiState() = HomeStateUi( + private val addTransactionModal = IvyModal() + + override val initialUi = HomeStateUi( period = null, trnsList = TransactionsListUi( upcoming = null, @@ -69,13 +76,18 @@ class HomeViewModel @Inject constructor( balance = ValueUi(amount = "0.0", currency = ""), income = ValueUi(amount = "0.0", currency = ""), expense = ValueUi(amount = "0.0", currency = ""), - hideBalance = false + hideBalance = false, + moreMenuVisible = false, + + addTransactionModal = addTransactionModal, ) // endregion private val overrideShowBalance = MutableStateFlow(false) + private val moreMenuVisible = MutableStateFlow(initialUi.moreMenuVisible) - override fun stateFlow(): Flow = combine( + // region State flow + override val stateFlow: Flow = combine( showBalanceFlow(), balanceFlow(), periodDataFlow() ) { showBalance, balance, periodData -> HomeState( @@ -97,22 +109,37 @@ class HomeViewModel @Inject constructor( emit(Value(amount = 0.0, currency = "")) } + @OptIn(ExperimentalCoroutinesApi::class) private fun periodDataFlow(): Flow = - baseCurrencyFlow().flatMapMerge { baseCurrency -> + baseCurrencyFlow().flatMapLatest { baseCurrency -> val selectedPeriodFlow = selectedPeriodFlow() // Trns History, Upcoming & Overdue - val trnsListFlow = selectedPeriodFlow.flatMapMerge { + val trnsListFlow = selectedPeriodFlow.flatMapLatest { val period = it.range() - trnsListFlow(ActualBetween(period) or DueBetween(period)) + // Due range: upcoming for this month + overdue for all time + val dueRange = TimeRange( + from = beginningOfIvyTime(), + to = period.to, + ) + trnsListFlow(ActualBetween(period) or DueBetween(dueRange)) } // Income & Expense for the period - val statsFlow = trnsListFlow.flatMapMerge { trnsList -> + val statsFlow = selectedPeriodFlow.flatMapLatest { + val period = it.range() + + // take only transactions from the history, excluding transfers + // but INCLUDING transfer fees + trnsFlow( + ActualBetween(period) and brackets( + ByPurpose(null) or ByPurpose(TrnPurpose.Fee) + ) + ) + }.flatMapLatest { trns -> calculateFlow( CalculateFlow.Input( - // take only transactions from the history, excluding transfers - trns = trnsList.history.mapNotNull { (it as? TrnListItem.Trn)?.trn }, + trns = trns, outputCurrency = baseCurrency, includeTransfers = false, includeHidden = false, @@ -133,21 +160,29 @@ class HomeViewModel @Inject constructor( } private fun showBalanceFlow(): Flow = combine( - hideBalanceSettingFlow(Unit), + hideBalanceFlow(Unit), overrideShowBalance ) { hideBalanceSettings, showBalance -> showBalance || !hideBalanceSettings } + // endregion - // region map to Ui state - override suspend fun mapToUiState(state: HomeState): HomeStateUi = HomeStateUi( - period = state.period?.let { mapSelectedPeriodUiAct(it) }, - trnsList = mapTransactionListUiAct(state.trnsList), - balance = formatBalance(state.balance), - income = format(state.income, shortenFiat = true), - expense = format(state.expense, shortenFiat = true), - hideBalance = state.hideBalance - ) + // region UI flow + override val uiFlow: Flow = combine( + stateFlow, moreMenuVisible + ) { state, moreMenuVisible -> + HomeStateUi( + period = state.period?.let { mapSelectedPeriodUiAct(it) }, + trnsList = mapTransactionListUiAct(state.trnsList), + balance = formatBalance(state.balance), + income = format(state.income, shortenFiat = true), + expense = format(state.expense, shortenFiat = true), + hideBalance = state.hideBalance, + moreMenuVisible = moreMenuVisible, + + addTransactionModal = addTransactionModal, + ) + } private fun formatBalance(balance: Value): ValueUi = format( value = balance, @@ -157,16 +192,41 @@ class HomeViewModel @Inject constructor( // region Event Handling override suspend fun handleEvent(event: HomeEvent) = when (event) { + is HomeEvent.BottomBarAction -> handleBottomBarAction(event.action) + HomeEvent.AddExpense -> handleAddExpense() + HomeEvent.AddIncome -> handleAddIncome() + HomeEvent.AddTransfer -> handleAddTransfer() HomeEvent.BalanceClick -> handleBalanceClick() HomeEvent.HiddenBalanceClick -> handleHiddenBalanceClick() - is HomeEvent.BottomBarAction -> handleBottomBarAction(event.action) HomeEvent.ExpenseClick -> handleExpenseClick() HomeEvent.IncomeClick -> handleIncomeClick() + HomeEvent.ShowBottomBar -> handleShowBottomBar() + HomeEvent.HideBottomBar -> handleHideBottomBar() + HomeEvent.MoreClick -> handleMoreClick() } - private fun handleBottomBarAction(action: HomeBottomBarAction) { - // TODO: Implement - navigator.navigate(Destination.debug.route) + private fun handleBottomBarAction(action: MainBottomBarAction) { + addTransactionModal.show() + } + + private fun handleAddTransfer() { + navigator.navigate(Destination.newTransfer.destination(Unit)) + } + + private fun handleAddIncome() { + navigator.navigate( + Destination.newTransaction.destination( + NewTransaction.Arg(trnType = TransactionType.Income) + ) + ) + } + + private fun handleAddExpense() { + navigator.navigate( + Destination.newTransaction.destination( + NewTransaction.Arg(trnType = TransactionType.Expense) + ) + ) } private fun handleBalanceClick() { @@ -186,6 +246,18 @@ class HomeViewModel @Inject constructor( delay(3_000L) overrideShowBalance.value = false } + + private fun handleShowBottomBar() { + mainBottomBarVisibility.visible.value = true + } + + private fun handleHideBottomBar() { + mainBottomBarVisibility.visible.value = false + } + + private fun handleMoreClick() { + moreMenuVisible.value = !moreMenuVisible.value + } // endregion private data class PeriodData( diff --git a/home/tab/src/main/java/com/ivy/home/components/Balance.kt b/home/tab/src/main/java/com/ivy/home/components/Balance.kt index a1f02c92fd..7f34e7dbbe 100644 --- a/home/tab/src/main/java/com/ivy/home/components/Balance.kt +++ b/home/tab/src/main/java/com/ivy/home/components/Balance.kt @@ -13,9 +13,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.value.AmountCurrency import com.ivy.design.l0_system.UI -import com.ivy.design.l1_buildingBlocks.B1Second -import com.ivy.design.l1_buildingBlocks.B2 import com.ivy.design.l1_buildingBlocks.H1Second import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.design.util.ComponentPreview @@ -62,18 +61,7 @@ internal fun BalanceMini( .clickable(onClick = onClick), verticalAlignment = Alignment.CenterVertically ) { - B2(text = "Balance", fontWeight = FontWeight.Normal) - SpacerHor(width = 4.dp) - B1Second( - text = balance.currency, - fontWeight = FontWeight.Normal, - ) - SpacerHor(width = 4.dp) - B1Second( - text = balance.amount, - modifier = Modifier.testTag("balance_mini_amount"), - fontWeight = FontWeight.Bold, - ) + AmountCurrency(balance) } } // endregion diff --git a/home/tab/src/main/java/com/ivy/home/components/IncomeExpense.kt b/home/tab/src/main/java/com/ivy/home/components/IncomeExpense.kt index a1f40b851e..ce1c4f9f98 100644 --- a/home/tab/src/main/java/com/ivy/home/components/IncomeExpense.kt +++ b/home/tab/src/main/java/com/ivy/home/components/IncomeExpense.kt @@ -12,18 +12,16 @@ 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.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.domain.pure.format.dummyValueUi -import com.ivy.core.ui.value.AmountCurrency import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.rememberContrastColor -import com.ivy.design.l1_buildingBlocks.Caption -import com.ivy.design.l1_buildingBlocks.IconRes -import com.ivy.design.l1_buildingBlocks.SpacerHor -import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.* import com.ivy.design.util.ComponentPreview import com.ivy.resources.R @@ -77,19 +75,22 @@ private fun Card( .clickable(onClick = onClick) .padding(all = 12.dp), ) { - val textColor = rememberContrastColor(bgColor) + val textColor = rememberContrast(bgColor) Row(verticalAlignment = Alignment.CenterVertically) { IconRes(icon = icon, tint = textColor) SpacerHor(width = 4.dp) Caption(text = text, color = textColor) } SpacerVer(height = 4.dp) - Row( - modifier = Modifier.padding(start = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - AmountCurrency(value = value, color = textColor) - } + // Amount + B1Second( + text = value.amount, + modifier = Modifier + .testTag("amount") + .padding(start = 8.dp), + fontWeight = FontWeight.Bold, + color = textColor, + ) } } diff --git a/home/tab/src/main/java/com/ivy/home/components/MoreMenuButton.kt b/home/tab/src/main/java/com/ivy/home/components/MoreMenuButton.kt index bdc06952cd..9a24bb852b 100644 --- a/home/tab/src/main/java/com/ivy/home/components/MoreMenuButton.kt +++ b/home/tab/src/main/java/com/ivy/home/components/MoreMenuButton.kt @@ -2,9 +2,9 @@ package com.ivy.home.components import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.ComponentPreview import com.ivy.home.R @@ -16,8 +16,8 @@ internal fun MoreMenuButton( ) { IvyButton( size = ButtonSize.Small, - visibility = ButtonVisibility.Medium, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Medium, + feeling = Feeling.Positive, text = null, icon = R.drawable.ic_settings, onClick = onClick, diff --git a/home/tab/src/main/java/com/ivy/home/event/HomeBottomBarAction.kt b/home/tab/src/main/java/com/ivy/home/event/HomeBottomBarAction.kt deleted file mode 100644 index f594b77657..0000000000 --- a/home/tab/src/main/java/com/ivy/home/event/HomeBottomBarAction.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.home.event - -enum class HomeBottomBarAction { - Click, SwipeUp, SwipeDiagonalLeft, SwipeDiagonalRight -} \ No newline at end of file diff --git a/home/tab/src/main/java/com/ivy/home/event/HomeEvent.kt b/home/tab/src/main/java/com/ivy/home/event/HomeEvent.kt deleted file mode 100644 index 91b3ffcf46..0000000000 --- a/home/tab/src/main/java/com/ivy/home/event/HomeEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.home.event - -sealed interface HomeEvent { - object BalanceClick : HomeEvent - object IncomeClick : HomeEvent - object ExpenseClick : HomeEvent - object HiddenBalanceClick : HomeEvent - - data class BottomBarAction(val action: HomeBottomBarAction) : HomeEvent -} \ No newline at end of file diff --git a/home/tab/src/main/java/com/ivy/home/modal/AddTransactionModal.kt b/home/tab/src/main/java/com/ivy/home/modal/AddTransactionModal.kt new file mode 100644 index 0000000000..e7da3dcdf1 --- /dev/null +++ b/home/tab/src/main/java/com/ivy/home/modal/AddTransactionModal.kt @@ -0,0 +1,91 @@ +package com.ivy.home.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.resources.R + +@Composable +internal fun BoxScope.AddTransactionModal( + modal: IvyModal, + onAddTransfer: () -> Unit, + onAddIncome: () -> Unit, + onAddExpense: () -> Unit, +) { + Modal( + modal = modal, + actions = {} + ) { + Title(text = "Add transaction") + SpacerVer(height = 24.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = stringResource(R.string.transfer), + icon = R.drawable.ic_transfer, + onClick = { + modal.hide() + onAddTransfer() + } + ) + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Custom(UI.colors.green), + text = stringResource(R.string.income), + icon = R.drawable.ic_income, + onClick = { + modal.hide() + onAddIncome() + } + ) + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Custom(UI.colors.red), + text = stringResource(R.string.expense), + icon = R.drawable.ic_expense, + onClick = { + modal.hide() + onAddExpense() + } + ) + SpacerVer(height = 16.dp) + } +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + AddTransactionModal( + modal = modal, + onAddTransfer = {}, + onAddIncome = {}, + onAddExpense = {}, + ) + } +} \ No newline at end of file diff --git a/home/tab/src/main/java/com/ivy/home/state/HomeStateUi.kt b/home/tab/src/main/java/com/ivy/home/state/HomeStateUi.kt index 954fe4a4e2..5cd201b978 100644 --- a/home/tab/src/main/java/com/ivy/home/state/HomeStateUi.kt +++ b/home/tab/src/main/java/com/ivy/home/state/HomeStateUi.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.ivy.core.domain.pure.format.ValueUi import com.ivy.core.ui.data.period.SelectedPeriodUi import com.ivy.core.ui.data.transaction.TransactionsListUi +import com.ivy.design.l2_components.modal.IvyModal @Immutable data class HomeStateUi( @@ -12,5 +13,8 @@ data class HomeStateUi( val balance: ValueUi, val income: ValueUi, val expense: ValueUi, - val hideBalance: Boolean + val hideBalance: Boolean, + val moreMenuVisible: Boolean, + + val addTransactionModal: IvyModal, ) \ No newline at end of file diff --git a/import-csv-backup/README.md b/import-csv-backup/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/import-csv-backup/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/import-csv-backup/build.gradle.kts b/import-csv-backup/build.gradle.kts deleted file mode 100644 index 9f22f75e34..0000000000 --- a/import-csv-backup/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` - `kotlin-android` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) - - implementation(project(":onboarding")) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/AndroidManifest.xml b/import-csv-backup/src/main/AndroidManifest.xml deleted file mode 100644 index 44a8bbd06c..0000000000 --- a/import-csv-backup/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/ImportScreen.kt b/import-csv-backup/src/main/java/com/ivy/import_data/ImportScreen.kt deleted file mode 100644 index 2c673c0a37..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/ImportScreen.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.ivy.import_data - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.design.util.IvyPreview - -import com.ivy.import_data.flow.ImportFrom -import com.ivy.import_data.flow.ImportProcessing -import com.ivy.import_data.flow.ImportResultUI -import com.ivy.import_data.flow.instructions.ImportInstructions -import com.ivy.onboarding.viewmodel.OnboardingViewModel -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportApp -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportResult - -@OptIn(ExperimentalStdlibApi::class) -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.ImportCSVScreen() { - val viewModel: ImportViewModel = hiltViewModel() - - val importStep by viewModel.importStep.observeAsState(ImportStep.IMPORT_FROM) - val importType by viewModel.importType.observeAsState() - val importProgressPercent by viewModel.importProgressPercent.observeAsState(0) - val importResult by viewModel.importResult.observeAsState() - - val onboardingViewModel: OnboardingViewModel = hiltViewModel() - - val context = LocalContext.current - - UI( -// screen = screen, - importStep = importStep, - importApp = importType, - importProgressPercent = importProgressPercent, - importResult = importResult, - - onChooseImportType = viewModel::setImportType, - onUploadCSVFile = { viewModel.uploadFile(context) }, - onSkip = { - viewModel.skip( - onboardingViewModel = onboardingViewModel - ) - }, - onFinish = { - viewModel.finish( - onboardingViewModel = onboardingViewModel - ) - } - ) -} - -@ExperimentalFoundationApi -@Composable -private fun BoxWithConstraintsScope.UI( -// screen: Import, - - importStep: ImportStep, - importApp: ImportApp?, - importProgressPercent: Int, - importResult: ImportResult?, - - onChooseImportType: (ImportApp) -> Unit = {}, - onUploadCSVFile: () -> Unit = {}, - onSkip: () -> Unit = {}, - onFinish: () -> Unit = {}, -) { - when (importStep) { - ImportStep.IMPORT_FROM -> { - ImportFrom( - hasSkip = false, //screen.launchedFromOnboarding, - onSkip = onSkip, - onImportFrom = onChooseImportType - ) - } - ImportStep.INSTRUCTIONS -> { - ImportInstructions( - hasSkip = false, //screen.launchedFromOnboarding, - importApp = importApp!!, - onSkip = onSkip, - onUploadClick = onUploadCSVFile - ) - } - ImportStep.LOADING -> { - ImportProcessing( - progressPercent = importProgressPercent - ) - } - ImportStep.RESULT -> { - ImportResultUI( - result = importResult!! - ) { - onFinish() - } - } - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview() { - IvyPreview { - UI( -// screen = Import(launchedFromOnboarding = true), - importStep = ImportStep.IMPORT_FROM, - importApp = null, - importProgressPercent = 0, - importResult = null - ) - } -} diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/ImportStep.kt b/import-csv-backup/src/main/java/com/ivy/import_data/ImportStep.kt deleted file mode 100644 index 2025d21b72..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/ImportStep.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.import_data - -enum class ImportStep { - IMPORT_FROM, INSTRUCTIONS, LOADING, RESULT -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/ImportViewModel.kt b/import-csv-backup/src/main/java/com/ivy/import_data/ImportViewModel.kt deleted file mode 100644 index 91f13981c1..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/ImportViewModel.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.ivy.import_data - -import android.content.Context -import android.net.Uri -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.frp.test.TestIdlingResource - -import com.ivy.onboarding.viewmodel.OnboardingViewModel -import com.ivy.wallet.domain.deprecated.logic.csv.CSVImporter -import com.ivy.wallet.domain.deprecated.logic.csv.CSVMapper -import com.ivy.wallet.domain.deprecated.logic.csv.CSVNormalizer -import com.ivy.wallet.domain.deprecated.logic.csv.IvyFileReader -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportApp -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportResult -import com.ivy.wallet.domain.deprecated.logic.zip.ExportZipLogic -import com.ivy.wallet.utils.asLiveData -import com.ivy.wallet.utils.getFileName -import com.ivy.wallet.utils.ioThread -import com.ivy.wallet.utils.uiThread -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import kotlin.math.roundToInt - -@HiltViewModel -class ImportViewModel @Inject constructor( - private val ivyContext: IvyWalletCtx, - private val - private val fileReader: IvyFileReader, - private val csvNormalizer: CSVNormalizer, - private val csvMapper: CSVMapper, - private val csvImporter: CSVImporter, - private val exportZipLogic: ExportZipLogic -) : ViewModel() { - private val _importStep = MutableLiveData() - val importStep = _importStep.asLiveData() - - private val _importApp = MutableLiveData() - val importType = _importApp.asLiveData() - - private val _importProgressPercent = MutableLiveData() - val importProgressPercent = _importProgressPercent.asLiveData() - - private val _importResult = MutableLiveData() - val importResult = _importResult.asLiveData() - - fun start() { -// nav.onBackPressed[screen] = { -// when (importStep.value) { -// ImportStep.IMPORT_FROM -> false -// ImportStep.INSTRUCTIONS -> { -// _importStep.value = ImportStep.IMPORT_FROM -// true -// } -// ImportStep.LOADING -> { -// //do nothing, disable back -// true -// } -// ImportStep.RESULT -> { -// _importStep.value = ImportStep.IMPORT_FROM -// true -// } -// null -> false -// } -// } - } - - @ExperimentalStdlibApi - fun uploadFile(context: Context) { - val importType = importType.value ?: return - - ivyContext.openFile { fileUri -> - viewModelScope.launch { - TestIdlingResource.increment() - - _importStep.value = ImportStep.LOADING - - _importResult.value = if (hasCSVExtension(context, fileUri)) - restoreCSVFile(fileUri = fileUri, importApp = importType) - else { - exportZipLogic.import( - context = context, - zipFileUri = fileUri, - onProgress = { progressPercent -> - uiThread { - _importProgressPercent.value = - (progressPercent * 100).roundToInt() - } - }) - } - - _importStep.value = ImportStep.RESULT - - TestIdlingResource.decrement() - } - } - } - - @ExperimentalStdlibApi - private suspend fun restoreCSVFile(fileUri: Uri, importApp: ImportApp): ImportResult { - return ioThread { - val rawCSV = fileReader.read( - uri = fileUri, - charset = when (importApp) { - ImportApp.IVY -> Charsets.UTF_16 - else -> Charsets.UTF_8 - } - ) - if (rawCSV == null || rawCSV.isBlank()) { - return@ioThread ImportResult( - rowsFound = 0, - transactionsImported = 0, - accountsImported = 0, - categoriesImported = 0, - failedRows = emptyList() - ) - } - - val normalizedCSV = csvNormalizer.normalize( - rawCSV = rawCSV, - importApp = importApp - ) - - val mapping = csvMapper.mapping( - type = importApp, - headerRow = normalizedCSV.split("\n").getOrNull(0) - ) - - return@ioThread try { - val result = csvImporter.import( - csv = normalizedCSV, - rowMapping = mapping, - onProgress = { progressPercent -> - uiThread { - _importProgressPercent.value = - (progressPercent * 100).roundToInt() - } - } - ) - - if (result.failedRows.isNotEmpty()) { - Timber.e("Import failed rows: ${result.failedRows}") - } - - result - } catch (e: Exception) { - e.printStackTrace() - ImportResult( - rowsFound = 0, - transactionsImported = 0, - accountsImported = 0, - categoriesImported = 0, - failedRows = emptyList() - ) - } - } - } - - fun setImportType(importApp: ImportApp) { - _importApp.value = importApp - _importStep.value = ImportStep.INSTRUCTIONS - } - - fun skip( - onboardingViewModel: OnboardingViewModel - ) { -// if (screen.launchedFromOnboarding) { -// onboardingViewModel.importSkip() -// } - - - resetState() - } - - fun finish( - onboardingViewModel: OnboardingViewModel - ) { -// if (screen.launchedFromOnboarding) { -// val importSuccess = importResult.value?.transactionsImported?.let { it > 0 } ?: false -// onboardingViewModel.importFinished( -// success = importSuccess -// ) -// } - - - resetState() - } - - private fun resetState() { - _importStep.value = ImportStep.IMPORT_FROM - } - - private suspend fun hasCSVExtension( - context: Context, - fileUri: Uri - ): Boolean = ioThread { - val fileName = context.getFileName(fileUri) - fileName?.endsWith(suffix = ".csv", ignoreCase = true) ?: false - } -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportFrom.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportFrom.kt deleted file mode 100644 index d86d9636ea..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportFrom.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.ivy.import_data.flow - - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.old.OnboardingToolbar -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportApp -import com.ivy.wallet.ui.theme.components.GradientCutBottom -import com.ivy.wallet.ui.theme.components.IvyIcon - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.ImportFrom( - hasSkip: Boolean, - - onSkip: () -> Unit = {}, - onImportFrom: (ImportApp) -> Unit = {}, -) { - val importApps = ImportApp.values() - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - ) { - stickyHeader { - - OnboardingToolbar( - hasSkip = hasSkip, - onBack = { nav.onBackPressed() }, - onSkip = onSkip - ) - //onboarding toolbar include paddingBottom 16.dp - } - - item { - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.import_from), - style = UI.typo.h2.style( - fontWeight = FontWeight.Black - ) - ) - - Spacer(Modifier.height(24.dp)) - } - - items(importApps) { - ImportOption( - importApp = it, - onImportFrom = onImportFrom - ) - } - - item { - //last spacer - Spacer(Modifier.height(96.dp)) - } - } - - GradientCutBottom( - height = 96.dp - ) -} - -@Composable -private fun ImportOption( - importApp: ImportApp, - onImportFrom: (ImportApp) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.rounded) - .background(UI.colors.medium, UI.shapes.rounded) - .clickable { - onImportFrom(importApp) - } - .padding(vertical = 24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - IvyIcon( - modifier = Modifier.size(32.dp), - icon = importApp.logo(), - tint = Color.Unspecified - ) - - Text( - modifier = Modifier.padding(start = 16.dp, end = 32.dp), - text = importApp.listName(), - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - color = UI.colorsInverted.pure - ) - ) - } - - Spacer(Modifier.height(8.dp)) -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview() { - IvyPreview { - ImportFrom( - hasSkip = true, - ) - } -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportProcessing.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportProcessing.kt deleted file mode 100644 index 042f560fc1..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportProcessing.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.ivy.import_data.flow - - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.GradientGreen -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.IvyDividerLine - -@Composable -fun ImportProcessing( - progressPercent: Int -) { - Column( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.height(80.dp)) - - Text( - text = stringResource(R.string.please_wait), - style = UI.typo.h2.style( - fontWeight = FontWeight.Black - ) - ) - - Spacer(Modifier.height(8.dp)) - - Text( - text = "${progressPercent}%", - style = UI.typo.b2.style( - color = Gray, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.height(24.dp)) - - IvyDividerLine( - modifier = Modifier.padding(horizontal = 24.dp) - ) - - Spacer(modifier = Modifier.weight(1f)) - - Text( - text = stringResource(R.string.importing_the_csv_file), - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.height(16.dp)) - - ProgressBar( - progressPercent = progressPercent - ) - - Spacer(Modifier.height(32.dp)) - } -} - -@Composable -private fun ProgressBar( - progressPercent: Int -) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(32.dp) - .padding(horizontal = 24.dp) - .background(UI.colors.medium, UI.shapes.fullyRounded), - verticalAlignment = Alignment.CenterVertically - ) { - if (progressPercent > 0) { - Spacer( - modifier = Modifier - .weight(progressPercent.toFloat()) - .height(32.dp) - .background(GradientGreen.asHorizontalBrush(), UI.shapes.fullyRounded), - ) - } - - val uncompletedPercent = 100 - progressPercent - if (uncompletedPercent > 0) { - Spacer( - modifier = Modifier - .weight(uncompletedPercent.toFloat()) - ) - } - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - ImportProcessing( - progressPercent = 49 - ) - } -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportResultUI.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportResultUI.kt deleted file mode 100644 index bdb289150a..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportResultUI.kt +++ /dev/null @@ -1,189 +0,0 @@ -package com.ivy.import_data.flow - -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportResult -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.BackButton -import com.ivy.wallet.ui.theme.components.IvyDividerLine -import com.ivy.wallet.ui.theme.components.OnboardingButton -import com.ivy.wallet.utils.format - -@Composable -fun ImportResultUI( - result: ImportResult, - - onFinish: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - ) { - Spacer(Modifier.height(16.dp)) - - - BackButton( - modifier = Modifier.padding(start = 20.dp) - ) { - nav.onBackPressed() - } - - Spacer(Modifier.height(24.dp)) - - val importSuccess = result.transactionsImported > 0 && - result.transactionsImported > result.rowsFound / 2 - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = if (importSuccess) stringResource(R.string.success) else stringResource(R.string.failure), - style = UI.typo.h2.style( - fontWeight = FontWeight.Black, - color = if (importSuccess) UI.colorsInverted.pure else Red - ) - ) - - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.imported), - style = UI.typo.b1.style( - color = Green, - fontWeight = FontWeight.Black - ) - ) - - Spacer(Modifier.height(8.dp)) - - val successPercent = if (result.rowsFound > 0) - (result.transactionsImported / result.rowsFound.toDouble()) * 100 else 0.0 - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = "${successPercent.format(2)}%", - style = UI.typoSecond.h2.style( - fontWeight = FontWeight.Normal - ) - ) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.transactions_imported, result.transactionsImported), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.Bold, - color = Gray - ) - ) - - Spacer(Modifier.height(4.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.accounts_imported, result.accountsImported), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.Bold, - color = Gray - ) - ) - - Spacer(Modifier.height(4.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.categories_imported, result.categoriesImported), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.Bold, - color = Gray - ) - ) - - - Spacer(Modifier.height(32.dp)) - - IvyDividerLine( - modifier = Modifier.padding(horizontal = 24.dp) - ) - - Spacer(Modifier.height(32.dp)) - - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.failed), - style = UI.typo.b1.style( - fontWeight = FontWeight.Black, - color = Red - ) - ) - - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = "${(100 - successPercent).format(2)}%", - style = UI.typoSecond.h2.style( - fontWeight = FontWeight.Normal - ) - ) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource( - R.string.rows_from_csv_not_recognized, - result.rowsFound - result.transactionsImported - ), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.Bold, - color = Gray - ) - ) - - //TODO: Implement "See failed imports" - - Spacer(Modifier.weight(1f)) - - OnboardingButton( - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - text = stringResource(R.string.finish), - textColor = White, - backgroundGradient = GradientIvy, - hasNext = true, - enabled = true - ) { - onFinish() - } - - Spacer(Modifier.height(16.dp)) - } -} - - -@Preview -@Composable -private fun Preview() { - IvyPreview { - ImportResultUI( - result = ImportResult( - rowsFound = 356, - transactionsImported = 320, - accountsImported = 4, - categoriesImported = 13, - failedRows = emptyList() - ) - ) { - - } - } -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportSteps.kt deleted file mode 100644 index 9fb5f93fe3..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/ImportSteps.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.ivy.import_data.flow - -import androidx.compose.runtime.Composable -import com.ivy.import_data.flow.instructions.* -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportApp -import com.ivy.wallet.ui.csvimport.flow.instructions.MoneyWalletSteps - -@Composable -fun ImportSteps( - type: ImportApp, - onUploadClick: () -> Unit -) { - when (type) { - ImportApp.IVY -> { - IvyWalletSteps( - onUploadClick = onUploadClick - ) - } - ImportApp.MONEY_MANAGER -> { - MoneyManagerPraseSteps( - onUploadClick = onUploadClick - ) - } - ImportApp.WALLET_BY_BUDGET_BAKERS -> { - WalletByBudgetBakersSteps( - onUploadClick = onUploadClick - ) - } - ImportApp.SPENDEE -> SpendeeSteps( - onUploadClick = onUploadClick - ) - ImportApp.MONEFY -> MonefySteps( - onUploadClick = onUploadClick - ) - ImportApp.ONE_MONEY -> OneMoneySteps( - onUploadClick = onUploadClick - ) - ImportApp.BLUE_COINS -> DefaultImportSteps( - onUploadClick = onUploadClick - ) - ImportApp.KTW_MONEY_MANAGER -> KTWMoneyManagerSteps( - onUploadClick = onUploadClick - ) - ImportApp.FORTUNE_CITY -> FortuneCitySteps( - onUploadClick = onUploadClick - ) - ImportApp.FINANCISTO -> FinancistoSteps( - onUploadClick = onUploadClick - ) - ImportApp.MONEY_WALLET -> MoneyWalletSteps( - onUploadClick = onUploadClick - ) - } -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/DefaultImportSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/DefaultImportSteps.kt deleted file mode 100644 index 7f600f8fc5..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/DefaultImportSteps.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.base.R - -@Composable -fun DefaultImportSteps( - videoUrl: String? = null, - articleUrl: String? = null, - - onUploadClick: () -> Unit -) { - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 1, - title = stringResource(R.string.export_csv_file) - ) - - Spacer(Modifier.height(12.dp)) - - VideoArticleRow( - videoUrl = videoUrl, - articleUrl = articleUrl - ) - - Spacer(Modifier.height(24.dp)) - - UploadFileStep( - stepNumber = 2, - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/FinancistoSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/FinancistoSteps.kt deleted file mode 100644 index f07c54c5a7..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/FinancistoSteps.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.base.R - -@Composable -fun FinancistoSteps( - onUploadClick: () -> Unit -) { - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 1, - title = stringResource(R.string.export_csv_file_standard), - description = stringResource(R.string.export_csv_file_standard_description) - ) - - Spacer(Modifier.height(24.dp)) - - UploadFileStep( - stepNumber = 2, - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/FortuneCitySteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/FortuneCitySteps.kt deleted file mode 100644 index 27c4059d10..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/FortuneCitySteps.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.runtime.Composable - -@Composable -fun FortuneCitySteps( - onUploadClick: () -> Unit -) { - DefaultImportSteps( - articleUrl = "https://fourdesire.helpshift.com/hc/en/5-fortune-city/faq/242-can-i-export-my-fortune-city-records/", - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/ImportInstructions.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/ImportInstructions.kt deleted file mode 100644 index 3494664308..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/ImportInstructions.kt +++ /dev/null @@ -1,369 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.FontWeight.Companion.Bold -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.import_data.flow.ImportSteps -import com.ivy.old.OnboardingToolbar -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportApp -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.components.GradientCutBottom -import com.ivy.wallet.ui.theme.components.IvyDividerLine -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.components.OnboardingButton -import com.ivy.wallet.utils.drawColoredShadow - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.ImportInstructions( - hasSkip: Boolean, - importApp: ImportApp, - - onSkip: () -> Unit, - onUploadClick: () -> Unit, -) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - ) { - stickyHeader { - - OnboardingToolbar( - hasSkip = hasSkip, - onBack = { nav.onBackPressed() }, - onSkip = onSkip - ) - //onboarding toolbar include paddingBottom 16.dp - } - - item { - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.how_to_import), - style = UI.typo.h2.style( - fontWeight = FontWeight.Black - ) - ) - - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.open), - style = UI.typo.b2.style( - color = Gray, - fontWeight = Bold - ) - ) - - Spacer(Modifier.height(24.dp)) - - App( - importApp = importApp - ) - - Spacer(Modifier.height(24.dp)) - - IvyDividerLine( - modifier = Modifier.padding(horizontal = 32.dp) - ) - } - - item { - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.steps), - style = UI.typo.b1.style( - fontWeight = FontWeight.Black - ) - ) - - ImportSteps( - type = importApp, - onUploadClick = onUploadClick - ) - } - - item { - //last spacer - Spacer(Modifier.height(96.dp)) - } - } - - GradientCutBottom( - height = 96.dp - ) -} - -@Composable -fun VideoArticleRow( - videoUrl: String?, - articleUrl: String? -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - val rootScreen = com.ivy.core.ui.temp.rootScreen() - - Spacer(Modifier.width(16.dp)) - - if (videoUrl != null) { - VideoButton( - modifier = Modifier.weight(1f) - ) { - rootScreen.openUrlInBrowser(videoUrl) - } - } - - if (videoUrl != null && articleUrl != null) { - Spacer(Modifier.width(8.dp)) - } - - if (articleUrl != null) { - ArticleButton( - modifier = Modifier.weight(1f) - ) { - rootScreen.openUrlInBrowser(articleUrl) - } - } - - Spacer(Modifier.width(16.dp)) - } -} - -@Composable -fun VideoButton( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - InstructionButton( - modifier = modifier, - icon = R.drawable.ic_import_video, - caption = stringResource(R.string.how_to), - text = stringResource(R.string.video) - ) { - onClick() - } -} - -@Composable -fun ArticleButton( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - InstructionButton( - modifier = modifier, - icon = R.drawable.ic_import_web, - caption = stringResource(R.string.how_to), - text = stringResource(R.string.article) - ) { - onClick() - } -} - -@Composable -fun InstructionButton( - modifier: Modifier = Modifier, - @DrawableRes icon: Int?, - caption: String, - text: String, - - onClick: () -> Unit -) { - Row( - modifier = modifier - .clip(UI.shapes.rounded) - .background(UI.colors.medium, UI.shapes.rounded) - .clickable { - onClick() - } - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - if (icon != null) { - IvyIcon( - modifier = Modifier.background(UI.colors.pure, CircleShape), - icon = icon, - tint = Color.Unspecified - ) - } - - Spacer(Modifier.width(if (icon != null) 24.dp else 12.dp)) - - Column { - Text( - text = caption, - style = UI.typo.c.style( - color = Gray, - fontWeight = Bold - ) - ) - - Spacer(Modifier.height(2.dp)) - - Text( - text = text, - style = UI.typo.b2.style( - fontWeight = Bold - ) - ) - } - } -} - -@Composable -fun UploadFileStep( - stepNumber: Int, - text: String = stringResource(R.string.upload_csv_file), - onUploadClick: () -> Unit -) { - StepTitle( - number = stepNumber, - title = text - ) - - Spacer(Modifier.height(16.dp)) - - OnboardingButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - text = text, - textColor = White, - backgroundGradient = GradientIvy, - hasNext = false, - iconStart = R.drawable.ic_upload - ) { - onUploadClick() - } -} - -@Composable -fun StepTitle( - number: Int, - title: String, - description: String? = null, -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - Text( - modifier = Modifier - .size(24.dp) - .background(UI.colors.medium, CircleShape), - text = number.toString(), - style = UI.typoSecond.b2.style( - fontWeight = Bold, - textAlign = TextAlign.Center - ) - ) - - Text( - modifier = Modifier - .weight(1f) - .padding(start = 8.dp, end = 32.dp), - text = title, - style = UI.typo.b2.style( - fontWeight = Bold - ) - ) - } - - if (description != null) { - Spacer(Modifier.height(4.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = description, - style = UI.typo.c.style( - fontWeight = Bold, - color = Gray - ) - ) - } - -} - -@Composable -private fun App( - importApp: ImportApp -) { - val rootScreen = com.ivy.core.ui.temp.rootScreen() - - Row( - modifier = Modifier - .padding(horizontal = 32.dp) - .clickable { - rootScreen.openGooglePlayAppPage( - appId = importApp.appId() - ) - }, - verticalAlignment = Alignment.CenterVertically - ) { - IvyIcon( - modifier = Modifier - .drawColoredShadow(importApp.color()) - .size(48.dp), - icon = importApp.logo(), - tint = Color.Unspecified - ) - - Spacer(Modifier.width(16.dp)) - - Text( - text = importApp.appName(), - style = UI.typo.b2.style( - fontWeight = Bold - ) - ) - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview() { - IvyPreview { - ImportInstructions( - hasSkip = true, - importApp = ImportApp.MONEY_MANAGER, - onSkip = {} - ) { - - } - } -} - diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/IvyWalletSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/IvyWalletSteps.kt deleted file mode 100644 index 6177ad3f38..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/IvyWalletSteps.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.base.R - -@Composable -fun IvyWalletSteps( - onUploadClick: () -> Unit -) { - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 1, - title = stringResource(R.string.export_data) - ) - - Spacer(Modifier.height(12.dp)) - - VideoArticleRow( - videoUrl = null, - articleUrl = null - ) - - Spacer(Modifier.height(24.dp)) - - UploadFileStep( - stepNumber = 2, - text = stringResource(R.string.upload_csv_zip_file), - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/KTWMoneyMangerSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/KTWMoneyMangerSteps.kt deleted file mode 100644 index baccb308f3..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/KTWMoneyMangerSteps.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.runtime.Composable - -@Composable -fun KTWMoneyManagerSteps( - onUploadClick: () -> Unit -) { - DefaultImportSteps( - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MonefySteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MonefySteps.kt deleted file mode 100644 index ce1ea386d2..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MonefySteps.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.base.R - -@Composable -fun MonefySteps( - onUploadClick: () -> Unit -) { - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 1, - title = stringResource(R.string.export_to_file), - description = stringResource(R.string.export_to_file_description) - ) - - Spacer(Modifier.height(24.dp)) - - UploadFileStep( - stepNumber = 2, - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MoneyManagerPraseSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MoneyManagerPraseSteps.kt deleted file mode 100644 index 619678c8dc..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MoneyManagerPraseSteps.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.base.R - -@Composable -fun MoneyManagerPraseSteps( - onUploadClick: () -> Unit -) { - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 1, - title = stringResource(R.string.export_excel_file), - ) - - Spacer(Modifier.height(12.dp)) - - VideoArticleRow( - videoUrl = null, - articleUrl = null - ) - - Spacer(Modifier.height(24.dp)) - - - StepTitle( - number = 2, - title = stringResource(R.string.convert_xls_to_csv), - description = stringResource(R.string.convert_xls_to_csv_description) - ) - - Spacer(Modifier.height(12.dp)) - - val rootScreen = com.ivy.core.ui.temp.rootScreen() - InstructionButton( - modifier = Modifier.padding(horizontal = 16.dp), - icon = null, - caption = stringResource(R.string.online_csv_converter_free), - text = "https://www.zamzar.com/converters/document/xls-to-csv/" - ) { - rootScreen.openUrlInBrowser("https://www.zamzar.com/converters/document/xls-to-csv/") - } - - - Spacer(Modifier.height(24.dp)) - - - UploadFileStep( - stepNumber = 3, - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MoneyWalletSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MoneyWalletSteps.kt deleted file mode 100644 index 6566e0b7e3..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/MoneyWalletSteps.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.ivy.wallet.ui.csvimport.flow.instructions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.import_data.flow.instructions.StepTitle -import com.ivy.import_data.flow.instructions.UploadFileStep - -@Composable -fun MoneyWalletSteps( - onUploadClick: () -> Unit -) { - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 1, - title = stringResource(R.string.export_csv_file), - description = stringResource(R.string.export_csv_moneywallet_description) - ) - - Spacer(Modifier.height(24.dp)) - - StepTitle( - number = 2, - title = stringResource(R.string.export_csv_moneywallet_rename_transfer_title), - description = stringResource(R.string.export_csv_moneywallet_rename_transfer_description) - ) - - Spacer(Modifier.height(24.dp)) - - UploadFileStep( - stepNumber = 3, - onUploadClick = onUploadClick - ) - - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.export_csv_caveats), - style = UI.typo.b1.style( - fontWeight = FontWeight.Black - ) - ) - - StepTitle( - number = 1, - title = stringResource(R.string.export_csv_moneywallet_caveat_1) - ) - - StepTitle( - number = 2, - title = stringResource(R.string.export_csv_moneywallet_caveat_2) - ) - -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/OneMoneySteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/OneMoneySteps.kt deleted file mode 100644 index 2922c135df..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/OneMoneySteps.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.runtime.Composable - -@Composable -fun OneMoneySteps( - onUploadClick: () -> Unit -) { - DefaultImportSteps( - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/SpendeeSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/SpendeeSteps.kt deleted file mode 100644 index cce7f7238d..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/SpendeeSteps.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.base.R - -@Composable -fun SpendeeSteps( - onUploadClick: () -> Unit -) { - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 1, - title = stringResource(R.string.export_csv_file) - ) - - Spacer(Modifier.height(12.dp)) - - VideoArticleRow( - videoUrl = null, - articleUrl = "https://help.spendee.com/article/137-export-transactions" - ) - - Spacer(Modifier.height(12.dp)) - - StepTitle( - number = 2, - title = stringResource(R.string.check_email_spam) - ) - - Spacer(Modifier.height(24.dp)) - - StepTitle( - number = 3, - title = stringResource(R.string.download_email_file), - description = stringResource(R.string.download_email_file_description) - ) - - Spacer(Modifier.height(24.dp)) - - UploadFileStep( - stepNumber = 4, - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/WalletByBudgetBakersSteps.kt b/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/WalletByBudgetBakersSteps.kt deleted file mode 100644 index 154c2938b6..0000000000 --- a/import-csv-backup/src/main/java/com/ivy/import_data/flow/instructions/WalletByBudgetBakersSteps.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.ivy.import_data.flow.instructions - -import androidx.compose.runtime.Composable - -@Composable -fun WalletByBudgetBakersSteps( - onUploadClick: () -> Unit -) { - DefaultImportSteps( - articleUrl = "https://support.budgetbakers.com/hc/en-us/articles/209753325-How-to-EXPORT-transactions-from-Wallet", - onUploadClick = onUploadClick - ) -} \ No newline at end of file diff --git a/item-transactions/README.md b/item-transactions/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/item-transactions/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/item-transactions/build.gradle.kts b/item-transactions/build.gradle.kts deleted file mode 100644 index 11ef563d20..0000000000 --- a/item-transactions/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) -} \ No newline at end of file diff --git a/item-transactions/src/main/AndroidManifest.xml b/item-transactions/src/main/AndroidManifest.xml deleted file mode 100644 index 20c942ac8f..0000000000 --- a/item-transactions/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/item-transactions/src/main/java/com/ivy/item_transactions/ItemStatisticScreen.kt b/item-transactions/src/main/java/com/ivy/item_transactions/ItemStatisticScreen.kt deleted file mode 100644 index d954e5bfee..0000000000 --- a/item-transactions/src/main/java/com/ivy/item_transactions/ItemStatisticScreen.kt +++ /dev/null @@ -1,766 +0,0 @@ -package com.ivy.item_transactions - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.* -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.toArgb -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.Constants -import com.ivy.base.R -import com.ivy.base.data.AppBaseData -import com.ivy.base.data.DueSection -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.pure.IncomeExpensePair -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - - -import com.ivy.old.IncomeExpensesCards -import com.ivy.old.ItemStatisticToolbar -import com.ivy.wallet.ui.component.transaction.transactions -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.BalanceRow -import com.ivy.wallet.ui.theme.components.BalanceRowMedium -import com.ivy.wallet.ui.theme.components.ItemIconMDefaultIcon -import com.ivy.wallet.ui.theme.modal.ChoosePeriodModal -import com.ivy.wallet.ui.theme.modal.ChoosePeriodModalData -import com.ivy.wallet.ui.theme.modal.DeleteModal -import com.ivy.wallet.ui.theme.modal.edit.AccountModal -import com.ivy.wallet.ui.theme.modal.edit.AccountModalData -import com.ivy.wallet.ui.theme.modal.edit.CategoryModal -import com.ivy.wallet.ui.theme.modal.edit.CategoryModalData -import com.ivy.wallet.ui.theme.wallet.PeriodSelector -import com.ivy.wallet.utils.* -import java.math.BigDecimal -import java.util.* - - -@Composable -fun BoxWithConstraintsScope.ItemStatisticScreen() { - val viewModel: ItemStatisticViewModel = hiltViewModel() - - - val period by viewModel.period.collectAsState() - val baseCurrency by viewModel.baseCurrency.collectAsState() - val currency by viewModel.currency.collectAsState() - - val account by viewModel.account.collectAsState() - val category by viewModel.category.collectAsState() - - val categories by viewModel.categories.collectAsState() - val isCategoryParentCategory by viewModel.isParentCategory.collectAsState() - val parentCategoryList by viewModel.parentCategoryList.collectAsState() - val accounts by viewModel.accounts.collectAsState() - - val balance by viewModel.balance.collectAsState() - val balanceBaseCurrency by viewModel.balanceBaseCurrency.collectAsState() - val income by viewModel.income.collectAsState() - val expenses by viewModel.expenses.collectAsState() - - val history by viewModel.history.collectAsState() - - val upcoming by viewModel.upcoming.collectAsState() - val upcomingExpanded by viewModel.upcomingExpanded.collectAsState() - val upcomingIncome by viewModel.upcomingIncome.collectAsState() - val upcomingExpenses by viewModel.upcomingExpenses.collectAsState() - - val overdue by viewModel.overdue.collectAsState() - val overdueExpanded by viewModel.overdueExpanded.collectAsState() - val overdueIncome by viewModel.overdueIncome.collectAsState() - val overdueExpenses by viewModel.overdueExpenses.collectAsState() - - val initWithTransactions by viewModel.initWithTransactions.collectAsState() - val treatTransfersAsIncomeExpense by viewModel.treatTransfersAsIncomeExpense.collectAsState() - - val view = LocalView.current - - UI( - period = period, - baseCurrency = baseCurrency, - currency = currency, - - categories = categories, - isCategoryParentCategory = isCategoryParentCategory, - parentCategoryList = parentCategoryList, - accounts = accounts, - - account = account, - category = category, - - balance = balance, - balanceBaseCurrency = balanceBaseCurrency, - income = income, - expenses = expenses, - - initWithTransactions = initWithTransactions, - treatTransfersAsIncomeExpense = treatTransfersAsIncomeExpense, - - history = history, - - upcoming = upcoming, - upcomingExpanded = upcomingExpanded, - setUpcomingExpanded = viewModel::setUpcomingExpanded, - upcomingIncome = upcomingIncome, - upcomingExpenses = upcomingExpenses, - - overdue = overdue, - overdueExpanded = overdueExpanded, - setOverdueExpanded = viewModel::setOverdueExpanded, - overdueIncome = overdueIncome, - overdueExpenses = overdueExpenses, - - - onSetPeriod = { - viewModel.setPeriod( - period = it - ) - }, - onNextMonth = { - viewModel.nextMonth() - }, - onPreviousMonth = { - viewModel.previousMonth() - }, - onDelete = { - viewModel.delete() - }, - onEditCategory = viewModel::editCategory, - onEditAccount = { acc, newBalance -> - viewModel.editAccount(acc, newBalance) - }, - onPayOrGet = { transaction -> - viewModel.payOrGet(transaction) - }, - onSkipTransaction = { transaction -> - viewModel.skipTransaction(transaction) - }, - onSkipAllTransactions = { transactions -> - viewModel.skipTransactions(transactions) - } - ) -} - -@Composable -private fun BoxWithConstraintsScope.UI( - period: TimePeriod, - baseCurrency: String, - currency: String, - - account: AccountOld?, - category: CategoryOld?, - - categories: List, - isCategoryParentCategory: Boolean = true, - parentCategoryList: List = emptyList(), - accounts: List, - - balance: Double, - balanceBaseCurrency: Double?, - income: Double, - expenses: Double, - - initWithTransactions: Boolean = false, - treatTransfersAsIncomeExpense: Boolean = false, - - history: List, - - upcomingExpanded: Boolean = true, - setUpcomingExpanded: (Boolean) -> Unit = {}, - upcomingIncome: Double = 0.0, - upcomingExpenses: Double = 0.0, - upcoming: List = emptyList(), - - overdueExpanded: Boolean = true, - setOverdueExpanded: (Boolean) -> Unit = {}, - overdueIncome: Double = 0.0, - overdueExpenses: Double = 0.0, - overdue: List = emptyList(), - - onPreviousMonth: () -> Unit, - onNextMonth: () -> Unit, - onSetPeriod: (TimePeriod) -> Unit, - onEditAccount: (AccountOld, Double) -> Unit, - onEditCategory: (CategoryOld) -> Unit, - onDelete: () -> Unit, - onPayOrGet: (TransactionOld) -> Unit = {}, - onSkipTransaction: (TransactionOld) -> Unit = {}, - onSkipAllTransactions: (List) -> Unit = {} -) { - - val itemColor = (account?.color ?: category?.color)?.toComposeColor() ?: Gray - - var deleteModalVisible by remember { mutableStateOf(false) } - var skipAllModalVisible by remember { mutableStateOf(false) } - var categoryModalData: CategoryModalData? by remember { mutableStateOf(null) } - var accountModalData: AccountModalData? by remember { mutableStateOf(null) } - var choosePeriodModal: ChoosePeriodModalData? by remember { mutableStateOf(null) } - - - Column( - modifier = Modifier - .fillMaxSize() - .background(itemColor) - .thenIf(!initWithTransactions) - { - horizontalSwipeListener( - sensitivity = 150, - onSwipeLeft = { - onNextMonth() - }, - onSwipeRight = { - onPreviousMonth() - } - ) - } - - ) { - val listState = rememberLazyListState() - val density = LocalDensity.current - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .padding(top = 16.dp) - .clip(UI.shapes.roundedTop) - .background(UI.colors.pure) - .testTag("item_stats_lazy_column"), - state = listState, - ) { - item { - Header( - history = history, - income = income, - expenses = expenses, - currency = currency, - baseCurrency = baseCurrency, - itemColor = itemColor, - account = account, - category = category, - balance = balance, - balanceBaseCurrency = balanceBaseCurrency, - treatTransfersAsIncomeExpense = treatTransfersAsIncomeExpense, - - onDelete = { - deleteModalVisible = true - }, - onEdit = { - when { - account != null -> { - accountModalData = AccountModalData( - account = account, - baseCurrency = currency, - balance = balance, - autoFocusKeyboard = false - ) - } - category != null -> { - categoryModalData = CategoryModalData( - category = category, - autoFocusKeyboard = false - ) - } - } - }, - - onBalanceClick = { - when { - account != null -> { - accountModalData = AccountModalData( - account = account, - baseCurrency = currency, - balance = balance, - adjustBalanceMode = true, - autoFocusKeyboard = false - ) - } - } - }, - showCategoryModal = { - categoryModalData = CategoryModalData( - category = category, - autoFocusKeyboard = false - ) - }, - showAccountModal = { - accountModalData = AccountModalData( - account = account, - baseCurrency = currency, - balance = balance, - adjustBalanceMode = false, - autoFocusKeyboard = false - ) - } - ) - } - - item { - //Rounded corners top effect - Box { - Spacer( - Modifier - .height(32.dp) - .fillMaxWidth() - .background(itemColor) //itemColor is displayed below the clip - .background(UI.colors.pure, UI.shapes.roundedTop) - ) - - PeriodSelector( - modifier = Modifier.padding(top = 16.dp), - period = period, - onPreviousMonth = { if (!initWithTransactions) onPreviousMonth() }, - onNextMonth = { if (!initWithTransactions) onNextMonth() }, - onShowChoosePeriodModal = { - if (!initWithTransactions) - choosePeriodModal = ChoosePeriodModalData( - period = period - ) - } - ) - } - } - - transactions( - baseData = AppBaseData( - baseCurrency, accounts, categories - ), - upcoming = DueSection( - trns = upcoming, - stats = IncomeExpensePair( - income = upcomingIncome.toBigDecimal(), - expense = upcomingExpenses.toBigDecimal() - ), - expanded = upcomingExpanded - ), - setUpcomingExpanded = setUpcomingExpanded, - - overdue = DueSection( - trns = overdue, - stats = IncomeExpensePair( - income = overdueIncome.toBigDecimal(), - expense = overdueExpenses.toBigDecimal() - ), - expanded = overdueExpanded - ), - setOverdueExpanded = setOverdueExpanded, - - history = history, - lastItemSpacer = 48.dp, - - onPayOrGet = onPayOrGet, - onSkipTransaction = onSkipTransaction, - onSkipAllTransactions = { skipAllModalVisible = true }, - emptyStateTitle = com.ivy.core.ui.temp.stringRes(R.string.no_transactions), - emptyStateText = com.ivy.core.ui.temp.stringRes( - R.string.no_transactions_for_period, - period.toDisplayLong(1) - ) - ) - } - } - - DeleteModal( - visible = deleteModalVisible, - title = stringResource(R.string.confirm_deletion), - description = if (account != null) { - stringResource(R.string.account_confirm_deletion_description) - } else { - stringResource(R.string.category_confirm_deletion_description) - }, - dismiss = { deleteModalVisible = false } - ) { - onDelete() - } - - DeleteModal( - visible = skipAllModalVisible, - title = stringResource(R.string.confirm_skip_all), - description = stringResource(R.string.confirm_skip_all_description), - dismiss = { skipAllModalVisible = false } - ) { - onSkipAllTransactions(overdue) - skipAllModalVisible = false - } - - CategoryModal( - modal = categoryModalData, - isCategoryParentCategory = isCategoryParentCategory, - parentCategoryList = parentCategoryList, - onCreateCategory = { }, - onEditCategory = onEditCategory, - dismiss = { - categoryModalData = null - } - ) - - AccountModal( - modal = accountModalData, - onCreateAccount = { }, - onEditAccount = onEditAccount, - dismiss = { - accountModalData = null - } - ) - - ChoosePeriodModal( - modal = choosePeriodModal, - dismiss = { - choosePeriodModal = null - } - ) { - onSetPeriod(it) - } -} - -@Composable -private fun Header( - history: List, - currency: String, - baseCurrency: String, - itemColor: Color, - account: AccountOld?, - category: CategoryOld?, - balance: Double, - balanceBaseCurrency: Double?, - income: Double, - expenses: Double, - treatTransfersAsIncomeExpense: Boolean = false, - - onEdit: () -> Unit, - onDelete: () -> Unit, - - onBalanceClick: () -> Unit, - showCategoryModal: () -> Unit, - showAccountModal: () -> Unit, -) { - val contrastColor = findContrastTextColor(itemColor) - - val darkColor = isDarkColor(itemColor) - setStatusBarDarkTextCompat(darkText = !darkColor) - - Column( - modifier = Modifier.background(itemColor) - ) { - Spacer(Modifier.height(20.dp)) - - ItemStatisticToolbar( - contrastColor = contrastColor, - onEdit = onEdit, - onDelete = onDelete - ) - - Spacer(Modifier.height(24.dp)) - - Item( - itemColor = itemColor, - contrastColor = contrastColor, - account = account, - category = category, - - showAccountModal = showAccountModal, - showCategoryModal = showCategoryModal - ) - - BalanceRow( - modifier = Modifier - .padding(start = 32.dp) - .testTag("balance") - .clickableNoIndication { - onBalanceClick() - }, - textColor = contrastColor, - currency = currency, - balance = balance, - balanceAmountPrefix = if (category != null) balancePrefix( - income = income, - expenses = expenses - ) else null - ) - - if (currency != baseCurrency && balanceBaseCurrency != null) { - BalanceRowMedium( - modifier = Modifier - .padding(start = 32.dp) - .clickableNoIndication { - onBalanceClick() - }, - textColor = itemColor.dynamicContrast(), - currency = baseCurrency, - balance = balanceBaseCurrency, - balanceAmountPrefix = if (category != null) balancePrefix( - income = income, - expenses = expenses - ) else null - ) - } - - Spacer(Modifier.height(20.dp)) - - - IncomeExpensesCards( - history = history, - currency = currency, - income = income, - expenses = expenses, - - hasAddButtons = true, - - itemColor = itemColor, - incomeHeaderCardClicked = { - if (account != null) { -// nav.navigateTo( -// PieChartStatistic( -// type = TrnTypeOld.INCOME, -// accountList = listOf(account.id), -// filterExcluded = false, -// treatTransfersAsIncomeExpense = treatTransfersAsIncomeExpense -// ) -// ) - } - }, - expenseHeaderCardClicked = { - if (account != null) { -// nav.navigateTo( -// PieChartStatistic( -// type = TrnTypeOld.EXPENSE, -// accountList = listOf(account.id), -// filterExcluded = false, -// treatTransfersAsIncomeExpense = treatTransfersAsIncomeExpense -// ) -// ) - } - } - ) { trnType -> -// nav.navigateTo( -// EditTransaction( -// initialTransactionId = null, -// type = trnType, -// accountId = account?.id, -// categoryId = category?.id -// ) -// ) - } - - Spacer(Modifier.height(20.dp)) - } -} - - -@Composable -private fun Item( - itemColor: Color, - contrastColor: Color, - account: AccountOld?, - category: CategoryOld?, - - showCategoryModal: () -> Unit, - showAccountModal: () -> Unit, -) { - Row( - modifier = Modifier - .padding(start = 22.dp) - .clickableNoIndication { - when { - account != null -> { - showAccountModal() - } - category != null -> { - showCategoryModal() - } - } - }, - verticalAlignment = Alignment.CenterVertically - ) { - when { - account != null -> { - ItemIconMDefaultIcon( - iconName = account.icon, - defaultIcon = R.drawable.ic_custom_account_m, - tint = contrastColor - ) - - Spacer(Modifier.width(8.dp)) - - Text( - text = account.name, - style = UI.typo.b1.style( - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - if (!account.includeInBalance) { - Spacer(Modifier.width(8.dp)) - - Text( - modifier = Modifier - .align(Alignment.Bottom) - .padding(bottom = 12.dp), - text = com.ivy.core.ui.temp.stringRes(R.string.excluded), - style = UI.typo.c.style( - color = account.color.toComposeColor().dynamicContrast() - ) - ) - } - } - category != null -> { - ItemIconMDefaultIcon( - iconName = category.icon, - defaultIcon = R.drawable.ic_custom_category_m, - tint = contrastColor - ) - - Spacer(Modifier.width(8.dp)) - - Text( - text = category.name, - style = UI.typo.b1.style( - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - ) - } - else -> { - //Unspecified - ItemIconMDefaultIcon( - iconName = null, - defaultIcon = R.drawable.ic_custom_category_m, - tint = contrastColor - ) - - Spacer(Modifier.width(8.dp)) - - Text( - text = Constants.CATEGORY_UNSPECIFIED_NAME, - style = UI.typo.b1.style( - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - ) - } - } - } -} - -@Preview -@Composable -private fun Preview_empty() { - IvyPreview { - UI( - period = TimePeriod.currentMonth( - startDayOfMonth = 1 - ), //preview - baseCurrency = "BGN", - currency = "BGN", - - categories = emptyList(), - accounts = emptyList(), - - balance = 1314.578, - balanceBaseCurrency = null, - income = 8000.0, - expenses = 6000.0, - - history = emptyList(), - category = null, - account = AccountOld("DSK", color = GreenDark.toArgb(), icon = "pet"), - onSetPeriod = { }, - onPreviousMonth = {}, - onNextMonth = {}, - onDelete = {}, - onEditAccount = { _, _ -> }, - onEditCategory = {} - ) - } -} - -@Preview -@Composable -private fun Preview_crypto() { - IvyPreview { - UI( - period = TimePeriod.currentMonth( - startDayOfMonth = 1 - ), //preview - baseCurrency = "BGN", - currency = "ADA", - - categories = emptyList(), - accounts = emptyList(), - - balance = 1314.578, - balanceBaseCurrency = 2879.28, - income = 8000.0, - expenses = 6000.0, - - history = emptyList(), - category = null, - account = AccountOld( - name = "DSK", - color = GreenDark.toArgb(), - icon = "pet", - includeInBalance = false - ), - onSetPeriod = { }, - onPreviousMonth = {}, - onNextMonth = {}, - onDelete = {}, - onEditAccount = { _, _ -> }, - onEditCategory = {} - ) - } -} - -@Preview -@Composable -private fun Preview_empty_upcoming() { - IvyPreview { - UI( - period = TimePeriod.currentMonth( - startDayOfMonth = 1 - ), //preview - baseCurrency = "BGN", - currency = "BGN", - - categories = emptyList(), - accounts = emptyList(), - - balance = 1314.578, - balanceBaseCurrency = null, - income = 8000.0, - expenses = 6000.0, - - history = emptyList(), - category = null, - account = AccountOld("DSK", color = GreenDark.toArgb(), icon = "pet"), - onSetPeriod = { }, - onPreviousMonth = {}, - onNextMonth = {}, - onDelete = {}, - onEditAccount = { _, _ -> }, - onEditCategory = {}, - upcoming = listOf( - TransactionOld(UUID(1L, 2L), TrnTypeOld.EXPENSE, BigDecimal.valueOf(10L)) - ) - ) - } -} \ No newline at end of file diff --git a/item-transactions/src/main/java/com/ivy/item_transactions/ItemStatisticViewModel.kt b/item-transactions/src/main/java/com/ivy/item_transactions/ItemStatisticViewModel.kt deleted file mode 100644 index b06bc4833a..0000000000 --- a/item-transactions/src/main/java/com/ivy/item_transactions/ItemStatisticViewModel.kt +++ /dev/null @@ -1,666 +0,0 @@ -package com.ivy.item_transactions - -import androidx.compose.ui.graphics.toArgb -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import arrow.core.toOption -import com.ivy.base.R -import com.ivy.base.toCloseTimeRange -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.frp.test.TestIdlingResource -import com.ivy.frp.then - -import com.ivy.temp.persistence.ExchangeActOld -import com.ivy.temp.persistence.ExchangeData -import com.ivy.temp.persistence.ExchangeRateDao -import com.ivy.wallet.domain.action.account.AccTrnsAct -import com.ivy.wallet.domain.action.account.AccountsActOld -import com.ivy.wallet.domain.action.account.CalcAccBalanceAct -import com.ivy.wallet.domain.action.account.CalcAccIncomeExpenseAct -import com.ivy.wallet.domain.action.category.CategoriesActOld -import com.ivy.wallet.domain.action.settings.BaseCurrencyActOld -import com.ivy.wallet.domain.action.transaction.CalcTrnsIncomeExpenseAct -import com.ivy.wallet.domain.action.transaction.TrnsWithDateDivsAct -import com.ivy.wallet.domain.deprecated.logic.* -import com.ivy.wallet.domain.deprecated.logic.currency.ExchangeRatesLogic -import com.ivy.wallet.domain.deprecated.sync.uploader.AccountUploader -import com.ivy.wallet.domain.deprecated.sync.uploader.CategoryUploader -import com.ivy.wallet.domain.pure.data.WalletDAOs -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.* -import com.ivy.wallet.ui.theme.RedLight -import com.ivy.wallet.utils.* -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import java.util.* -import javax.inject.Inject - -@HiltViewModel -class ItemStatisticViewModel @Inject constructor( - private val walletDAOs: WalletDAOs, - private val accountDao: AccountDao, - private val exchangeRateDao: ExchangeRateDao, - private val transactionDao: TransactionDao, - private val categoryDao: CategoryDao, - private val settingsDao: SettingsDao, - private val ivyContext: IvyWalletCtx, - private val - private val categoryUploader: CategoryUploader, - private val accountUploader: AccountUploader, - private val accountLogic: WalletAccountLogic, - private val categoryLogic: WalletCategoryLogic, - private val plannedPaymentRuleDao: PlannedPaymentRuleDao, - private val categoryCreator: CategoryCreator, - private val accountCreator: AccountCreator, - private val plannedPaymentsLogic: PlannedPaymentsLogic, - private val exchangeRatesLogic: ExchangeRatesLogic, - private val sharedPrefs: SharedPrefs, - private val categoriesAct: CategoriesActOld, - private val accountsAct: AccountsActOld, - private val accTrnsAct: AccTrnsAct, - private val trnsWithDateDivsAct: TrnsWithDateDivsAct, - private val baseCurrencyAct: BaseCurrencyActOld, - private val calcAccBalanceAct: CalcAccBalanceAct, - private val calcAccIncomeExpenseAct: CalcAccIncomeExpenseAct, - private val calcTrnsIncomeExpenseAct: CalcTrnsIncomeExpenseAct, - private val exchangeAct: ExchangeActOld -) : ViewModel() { - - private val _period = MutableStateFlow(ivyContext.selectedPeriod) - val period = _period.readOnly() - - private val _categories = MutableStateFlow>(emptyList()) - val categories = _categories.readOnly() - - private val _accounts = MutableStateFlow>(emptyList()) - val accounts = _accounts.readOnly() - - private val _baseCurrency = MutableStateFlow("") - val baseCurrency = _baseCurrency.readOnly() - - private val _currency = MutableStateFlow("") - val currency = _currency.readOnly() - - private val _balance = MutableStateFlow(0.0) - val balance = _balance.readOnly() - - private val _balanceBaseCurrency = MutableStateFlow(null) - val balanceBaseCurrency = _balanceBaseCurrency.readOnly() - - private val _income = MutableStateFlow(0.0) - val income = _income.readOnly() - - private val _expenses = MutableStateFlow(0.0) - val expenses = _expenses.readOnly() - - //Upcoming - private val _upcoming = MutableStateFlow>(emptyList()) - val upcoming = _upcoming.readOnly() - - private val _upcomingIncome = MutableStateFlow(0.0) - val upcomingIncome = _upcomingIncome.readOnly() - - private val _upcomingExpenses = MutableStateFlow(0.0) - val upcomingExpenses = _upcomingExpenses.readOnly() - - private val _upcomingExpanded = MutableStateFlow(false) - val upcomingExpanded = _upcomingExpanded.readOnly() - - //Overdue - private val _overdue = MutableStateFlow>(emptyList()) - val overdue = _overdue.readOnly() - - private val _overdueIncome = MutableStateFlow(0.0) - val overdueIncome = _overdueIncome.readOnly() - - private val _overdueExpenses = MutableStateFlow(0.0) - val overdueExpenses = _overdueExpenses.readOnly() - - private val _overdueExpanded = MutableStateFlow(true) - val overdueExpanded = _overdueExpanded.readOnly() - - //History - private val _history = MutableStateFlow>(emptyList()) - val history = _history.readOnly() - - private val _account = MutableStateFlow(null) - val account = _account.readOnly() - - private val _category = MutableStateFlow(null) - val category = _category.readOnly() - - private val _isParentCategory = MutableStateFlow(false) - val isParentCategory = _isParentCategory.readOnly() - - private val _parentCategoryList = MutableStateFlow>(emptyList()) - val parentCategoryList = _parentCategoryList.readOnly() - - private val _initWithTransactions = MutableStateFlow(false) - val initWithTransactions = _initWithTransactions.readOnly() - - private val _treatTransfersAsIncomeExpense = MutableStateFlow(false) - val treatTransfersAsIncomeExpense = _treatTransfersAsIncomeExpense.readOnly() - - fun start( - period: TimePeriod? = ivyContext.selectedPeriod, - reset: Boolean = true - ) { - TestIdlingResource.increment() - - if (reset) { - reset() - } - - viewModelScope.launch { - _period.value = period ?: ivyContext.selectedPeriod - - val baseCurrency = baseCurrencyAct(Unit) - _baseCurrency.value = baseCurrency - _currency.value = baseCurrency - - _categories.value = categoriesAct(Unit) - _accounts.value = accountsAct(Unit) - _initWithTransactions.value = false - _treatTransfersAsIncomeExpense.value = - sharedPrefs.getBoolean(SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, false) - -// when { -// screen.accountId != null -> { -// initForAccount(screen.accountId!!) -// } -// screen.categoryId != null && screen.transactions.isEmpty() -> { -// initForCategory(screen.categoryId!!, screen.accountIdFilterList) -// } -// //unspecifiedCategory==false is explicitly checked to accommodate for a temp AccountTransfers Category during Reports Screen -// screen.categoryId != null && screen.transactions.isNotEmpty() -// && screen.unspecifiedCategory == false -> { -// initForCategoryWithTransactions( -// screen.categoryId!!, -// screen.accountIdFilterList, -// screen.transactions -// ) -// } -// screen.unspecifiedCategory == true && screen.transactions.isNotEmpty() -> { -// initForAccountTransfersCategory( -// screen.categoryId, -// screen.accountIdFilterList, -// screen.transactions -// ) -// } -// screen.unspecifiedCategory == true -> { -// initForUnspecifiedCategory() -// } -// else -> error("no id provided") -// } - } - - TestIdlingResource.decrement() - } - - private suspend fun initForAccount(accountId: UUID) { - val account = ioThread { - accountDao.findById(accountId)?.toDomain() ?: error("account not found") - } - _account.value = account - val range = period.value.toRange(ivyContext.startDayOfMonth) - - if (account.currency.isNotNullOrBlank()) { - _currency.value = account.currency!! - } - - val balance = calcAccBalanceAct( - CalcAccBalanceAct.Input( - account = account - ) - ).balance.toDouble() - _balance.value = balance - if (baseCurrency.value != currency.value) { - _balanceBaseCurrency.value = exchangeAct( - ExchangeActOld.Input( - data = ExchangeData( - baseCurrency = baseCurrency.value, - fromCurrency = currency.value.toOption() - ), - amount = balance.toBigDecimal() - ) - ).orNull()?.toDouble() - } - - val includeTransfersInCalc = - sharedPrefs.getBoolean(SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, false) - - val incomeExpensePair = calcAccIncomeExpenseAct( - CalcAccIncomeExpenseAct.Input( - account = account, - range = range.toCloseTimeRange(), - includeTransfersInCalc = includeTransfersInCalc - ) - ).incomeExpensePair - _income.value = incomeExpensePair.income.toDouble() - _expenses.value = incomeExpensePair.expense.toDouble() - - _history.value = (accTrnsAct then { - trnsWithDateDivsAct( - TrnsWithDateDivsAct.Input( - baseCurrency = baseCurrency.value, - transactions = it - ) - ) - })( - AccTrnsAct.Input( - accountId = account.id, - range = range.toCloseTimeRange() - ) - ) - - //Upcoming - _upcomingIncome.value = ioThread { - accountLogic.calculateUpcomingIncome(account, range) - } - - _upcomingExpenses.value = ioThread { - accountLogic.calculateUpcomingExpenses(account, range) - } - - _upcoming.value = ioThread { accountLogic.upcoming(account, range) } - - //Overdue - _overdueIncome.value = ioThread { - accountLogic.calculateOverdueIncome(account, range) - } - - _overdueExpenses.value = ioThread { - accountLogic.calculateOverdueExpenses(account, range) - } - - _overdue.value = ioThread { accountLogic.overdue(account, range) } - } - - private suspend fun initForCategory(categoryId: UUID, accountFilterList: List) { - val accountFilterSet = accountFilterList.toSet() - val category = ioThread { - categoryDao.findById(categoryId)?.toDomain() ?: error("category not found") - } - _category.value = category - _isParentCategory.value = - ioThread { categoryDao.findAllSubCategories(category.id).isNotEmpty() } - - _parentCategoryList.value = - ioThread { categoryDao.findAllParentCategories().map { it.toDomain() } } - - val range = period.value.toRange(ivyContext.startDayOfMonth) - - _balance.value = ioThread { - categoryLogic.calculateCategoryBalance(category, range, accountFilterSet) - } - - _income.value = ioThread { - categoryLogic.calculateCategoryIncome(category, range, accountFilterSet) - } - - _expenses.value = ioThread { - categoryLogic.calculateCategoryExpenses(category, range, accountFilterSet) - } - - _history.value = ioThread { - categoryLogic.historyByCategoryAccountWithDateDividers( - category, - range, - accountFilterSet = accountFilterList.toSet(), - ) - } - - //Upcoming - //TODO: Rework Upcoming to FP - _upcomingIncome.value = ioThread { - categoryLogic.calculateUpcomingIncomeByCategory(category, range) - } - - _upcomingExpenses.value = ioThread { - categoryLogic.calculateUpcomingExpensesByCategory(category, range) - } - - _upcoming.value = ioThread { categoryLogic.upcomingByCategory(category, range) } - - //Overdue - //TODO: Rework Overdue to FP - _overdueIncome.value = ioThread { - categoryLogic.calculateOverdueIncomeByCategory(category, range) - } - - _overdueExpenses.value = ioThread { - categoryLogic.calculateOverdueExpensesByCategory(category, range) - } - - _overdue.value = ioThread { categoryLogic.overdueByCategory(category, range) } - } - - private suspend fun initForCategoryWithTransactions( - categoryId: UUID, - accountFilterList: List, - transactions: List - ) { - computationThread { - _initWithTransactions.value = true - - val trans = transactions.filter { - it.type != TrnTypeOld.TRANSFER && it.categoryId == categoryId - } - - val accountFilterSet = accountFilterList.toSet() - val category = ioThread { - categoryDao.findById(categoryId)?.toDomain() ?: error("category not found") - } - _category.value = category - val range = period.value.toRange(ivyContext.startDayOfMonth) - - val incomeTrans = transactions.filter { - it.categoryId == categoryId && it.type == TrnTypeOld.INCOME - } - - val expenseTrans = transactions.filter { - it.categoryId == categoryId && it.type == TrnTypeOld.EXPENSE - } - - _balance.value = ioThread { - categoryLogic.calculateCategoryBalance( - category, - range, - accountFilterSet, - transactions = trans - ) - } - - _income.value = ioThread { - categoryLogic.calculateCategoryIncome( - incomeTransaction = incomeTrans, - accountFilterSet = accountFilterSet - ) - } - - _expenses.value = ioThread { - categoryLogic.calculateCategoryExpenses( - expenseTransactions = expenseTrans, - accountFilterSet = accountFilterSet - ) - } - - _history.value = ioThread { - categoryLogic.historyByCategoryAccountWithDateDividers( - category, - range, - accountFilterSet = accountFilterList.toSet(), - transactions = trans - ) - } - - //Upcoming - //TODO: Rework Upcoming to FP - _upcomingIncome.value = ioThread { - categoryLogic.calculateUpcomingIncomeByCategory(category, range) - } - - _upcomingExpenses.value = ioThread { - categoryLogic.calculateUpcomingExpensesByCategory(category, range) - } - - _upcoming.value = ioThread { categoryLogic.upcomingByCategory(category, range) } - - //Overdue - //TODO: Rework Overdue to FP - _overdueIncome.value = ioThread { - categoryLogic.calculateOverdueIncomeByCategory(category, range) - } - - _overdueExpenses.value = ioThread { - categoryLogic.calculateOverdueExpensesByCategory(category, range) - } - - _overdue.value = ioThread { categoryLogic.overdueByCategory(category, range) } - } - } - - private suspend fun initForUnspecifiedCategory() { - val range = period.value.toRange(ivyContext.startDayOfMonth) - - _balance.value = ioThread { - categoryLogic.calculateUnspecifiedBalance(range) - } - - _income.value = ioThread { - categoryLogic.calculateUnspecifiedIncome(range) - } - - _expenses.value = ioThread { - categoryLogic.calculateUnspecifiedExpenses(range) - } - - _history.value = ioThread { - categoryLogic.historyUnspecified(range) - } - - //Upcoming - _upcomingIncome.value = ioThread { - categoryLogic.calculateUpcomingIncomeUnspecified(range) - } - - _upcomingExpenses.value = ioThread { - categoryLogic.calculateUpcomingExpensesUnspecified(range) - } - - _upcoming.value = ioThread { categoryLogic.upcomingUnspecified(range) } - - //Overdue - _overdueIncome.value = ioThread { - categoryLogic.calculateOverdueIncomeUnspecified(range) - } - - _overdueExpenses.value = ioThread { - categoryLogic.calculateOverdueExpensesUnspecified(range) - } - - _overdue.value = ioThread { categoryLogic.overdueUnspecified(range) } - } - - private suspend fun initForAccountTransfersCategory( - categoryId: UUID?, - accountFilterList: List, - transactions: List - ) { - _initWithTransactions.value = true - _category.value = - CategoryOld( - com.ivy.core.ui.temp.stringRes(R.string.account_transfers), - RedLight.toArgb(), - "transfer" - ) - val accountFilterIdSet = accountFilterList.toHashSet() - val trans = transactions.filter { - it.categoryId == null && (accountFilterIdSet.contains(it.accountId) || accountFilterIdSet.contains( - it.toAccountId - )) && it.type == TrnTypeOld.TRANSFER - } - - val historyIncomeExpense = calcTrnsIncomeExpenseAct( - CalcTrnsIncomeExpenseAct.Input( - transactions = trans, - accounts = accountFilterList.mapNotNull { accID -> accounts.value.find { it.id == accID } }, - baseCurrency = baseCurrency.value - ) - ) - - _income.value = historyIncomeExpense.transferIncome.toDouble() - _expenses.value = historyIncomeExpense.transferExpense.toDouble() - _balance.value = _income.value - _expenses.value - _history.value = trnsWithDateDivsAct( - TrnsWithDateDivsAct.Input( - baseCurrency = baseCurrency.value, - transactions = transactions - ) - ) - } - - private fun reset() { - _account.value = null - _category.value = null - } - - fun setUpcomingExpanded(expanded: Boolean) { - _upcomingExpanded.value = expanded - } - - fun setOverdueExpanded(expanded: Boolean) { - _overdueExpanded.value = expanded - } - - fun setPeriod( - period: TimePeriod - ) { - start( - period = period, - reset = false - ) - } - - fun nextMonth() { - val month = period.value.month - val year = period.value.year ?: dateNowUTC().year - if (month != null) { - start( - period = month.incrementMonthPeriod(ivyContext, 1L, year), - reset = false - ) - } - } - - fun previousMonth() { - val month = period.value.month - val year = period.value.year ?: dateNowUTC().year - if (month != null) { - start( - period = month.incrementMonthPeriod(ivyContext, -1L, year), - reset = false - ) - } - } - - fun delete() { - viewModelScope.launch { - TestIdlingResource.increment() - - when { -// screen.accountId != null -> { -// deleteAccount(screen.accountId!!) -// } -// screen.categoryId != null -> { -// deleteCategory(screen.categoryId!!) -// } - } - - TestIdlingResource.decrement() - } - } - - private suspend fun deleteAccount(accountId: UUID) { - ioThread { - transactionDao.flagDeletedByAccountId(accountId = accountId) - plannedPaymentRuleDao.flagDeletedByAccountId(accountId = accountId) - accountDao.flagDeleted(accountId) - - - //the server deletes transactions + planned payments for the account - accountUploader.delete(accountId) - } - } - - private suspend fun deleteCategory(categoryId: UUID) { - ioThread { - categoryDao.flagDeleted(categoryId) - - - - categoryUploader.delete(categoryId) - } - } - - fun editCategory(updatedCategory: CategoryOld) { - viewModelScope.launch { - TestIdlingResource.increment() - - categoryCreator.editCategory(updatedCategory) { - _category.value = it - } - - TestIdlingResource.decrement() - } - } - - fun editAccount(account: AccountOld, newBalance: Double) { - viewModelScope.launch { - TestIdlingResource.increment() - - accountCreator.editAccount(account, newBalance) { - start( - period = period.value, - reset = false - ) - } - - TestIdlingResource.decrement() - } - } - - fun payOrGet(transaction: TransactionOld) { - viewModelScope.launch { - TestIdlingResource.increment() - - plannedPaymentsLogic.payOrGet(transaction = transaction) { - start( - reset = false - ) - } - - TestIdlingResource.decrement() - } - } - - fun skipTransaction(transaction: TransactionOld) { - viewModelScope.launch { - TestIdlingResource.increment() - - plannedPaymentsLogic.payOrGet( - transaction = transaction, - skipTransaction = true - ) { - start( - reset = false - ) - } - - TestIdlingResource.decrement() - } - } - - fun skipTransactions(transactions: List) { - viewModelScope.launch { - TestIdlingResource.increment() - - plannedPaymentsLogic.payOrGet( - transactions = transactions, - skipTransaction = true - ) { - start( - reset = false - ) - } - - TestIdlingResource.decrement() - } - } -} \ No newline at end of file diff --git a/loans/README.md b/loans/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/loans/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/loans/build.gradle.kts b/loans/build.gradle.kts deleted file mode 100644 index f8e1b2d53a..0000000000 --- a/loans/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -import com.ivy.buildsrc.EventBus -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) - EventBus() -} \ No newline at end of file diff --git a/loans/src/main/AndroidManifest.xml b/loans/src/main/AndroidManifest.xml deleted file mode 100644 index 636a915789..0000000000 --- a/loans/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/loans/src/main/java/com/ivy/loans/loan/LoanBottomBar.kt b/loans/src/main/java/com/ivy/loans/loan/LoanBottomBar.kt deleted file mode 100644 index 0452060d43..0000000000 --- a/loans/src/main/java/com/ivy/loans/loan/LoanBottomBar.kt +++ /dev/null @@ -1,49 +0,0 @@ -//package com.ivy.wallet.ui.loan -// -//import androidx.compose.foundation.background -//import androidx.compose.foundation.layout.BoxWithConstraintsScope -//import androidx.compose.foundation.layout.Column -//import androidx.compose.foundation.layout.fillMaxSize -//import androidx.compose.runtime.Composable -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.res.stringResource -//import androidx.compose.ui.tooling.preview.Preview -//import com.ivy.base.R -//import com.ivy.design.util.IvyPreview -//import com.ivy.wallet.ui.theme.Blue -//import com.ivy.wallet.ui.theme.components.BackBottomBar -//import com.ivy.wallet.ui.theme.components.IvyButton -// -//@Composable -//internal fun BoxWithConstraintsScope.LoanBottomBar( -// onClose: () -> Unit, -// onAdd: () -> Unit -//) { -// BackBottomBar(onBack = onClose) { -// IvyButton( -// text = stringResource(R.string.add_loan), -// iconStart = R.drawable.ic_plus -// ) { -// onAdd() -// } -// } -//} -// -//@Preview -//@Composable -//private fun PreviewBottomBar() { -// IvyPreview { -// Column( -// Modifier -// .fillMaxSize() -// .background(Blue) -// ) { -// -// } -// -// LoanBottomBar( -// onAdd = {}, -// onClose = {} -// ) -// } -//} \ No newline at end of file diff --git a/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt b/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt deleted file mode 100644 index 7611208e0b..0000000000 --- a/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt +++ /dev/null @@ -1,272 +0,0 @@ -//package com.ivy.wallet.ui.loan -// -//import androidx.lifecycle.ViewModel -//import androidx.lifecycle.viewModelScope -//import com.ivy.data.AccountOld -//import com.ivy.data.getDefaultFIATCurrency -//import com.ivy.data.loan.Loan -//import com.ivy.frp.test.TestIdlingResource -//import com.ivy.temp.event.AccountsUpdatedEvent -//import com.ivy.wallet.domain.action.account.AccountsActOld -//import com.ivy.wallet.domain.action.category.CategoriesActOld -//import com.ivy.wallet.domain.action.loan.LoansAct -//import com.ivy.wallet.domain.deprecated.logic.loantrasactions.LoanTransactionsLogic -//import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -//import com.ivy.wallet.domain.deprecated.logic.model.CreateLoanData -//import com.ivy.wallet.domain.deprecated.sync.item.LoanSync -//import com.ivy.wallet.io.persistence.SharedPrefs -//import com.ivy.wallet.io.persistence.dao.AccountDao -//import com.ivy.wallet.io.persistence.dao.LoanDao -//import com.ivy.wallet.io.persistence.dao.LoanRecordDao -//import com.ivy.wallet.io.persistence.dao.SettingsDao -//import com.ivy.wallet.io.persistence.data.toEntity -//import com.ivy.wallet.ui.loan.data.DisplayLoan -//import com.ivy.wallet.ui.theme.modal.LoanModalData -//import com.ivy.wallet.utils.format -//import com.ivy.wallet.utils.ioThread -//import dagger.hilt.android.lifecycle.HiltViewModel -//import kotlinx.coroutines.Dispatchers -//import kotlinx.coroutines.flow.MutableStateFlow -//import kotlinx.coroutines.flow.StateFlow -//import kotlinx.coroutines.flow.asStateFlow -//import kotlinx.coroutines.launch -//import org.greenrobot.eventbus.EventBus -//import java.util.* -//import javax.inject.Inject -// -//@HiltViewModel -//class LoanViewModel @Inject constructor( -// private val loanDao: LoanDao, -// private val loanRecordDao: LoanRecordDao, -// private val settingsDao: SettingsDao, -// private val loanSync: LoanSync, -// private val loanCreator: LoanCreator, -// private val sharedPrefs: SharedPrefs, -// private val accountDao: AccountDao, -// private val accountCreator: AccountCreator, -// private val loanTransactionsLogic: LoanTransactionsLogic, -// private val loansAct: LoansAct, -// private val accountsAct: AccountsActOld, -// private val categoriesAct: CategoriesActOld -//) : ViewModel() { -// -// private val _baseCurrencyCode = MutableStateFlow(getDefaultFIATCurrency().currencyCode) -// val baseCurrencyCode = _baseCurrencyCode.asStateFlow() -// -// private val _loans = MutableStateFlow(emptyList()) -// val loans = _loans.asStateFlow() -// -// private val _accounts = MutableStateFlow>(emptyList()) -// val accounts = _accounts.asStateFlow() -// -// private val _selectedAccount = MutableStateFlow(null) -// val selectedAccount = _selectedAccount.asStateFlow() -// -// private var defaultCurrencyCode = "" -// -// private val _state = MutableStateFlow(LoanScreenState()) -// val state: StateFlow = _state -// -// fun start() { -// viewModelScope.launch(Dispatchers.Default) { -// TestIdlingResource.increment() -// -// defaultCurrencyCode = ioThread { -// settingsDao.findFirstSuspend().currency -// }.also { -// _baseCurrencyCode.value = it -// } -// -// initialiseAccounts() -// -// _loans.value = ioThread { -// loansAct(Unit) -// .map { loan -> -// val amountPaid = calculateAmountPaid(loan) -// val loanAmount = loan.amount -// val percentPaid = amountPaid / loanAmount -// val currCode = findCurrencyCode(accounts.value, loan.accountId) -// -// DisplayLoan( -// loan = loan, -// amountPaid = amountPaid, -// currencyCode = currCode, -// formattedDisplayText = "${amountPaid.format(currCode)} $currCode / ${ -// loanAmount.format( -// currCode -// ) -// } $currCode (${ -// percentPaid.times( -// 100 -// ).format(2) -// }%)", -// percentPaid = percentPaid -// ) -// } -// } -// _state.value = LoanScreenState( -// baseCurrency = defaultCurrencyCode, -// loans = _loans.value, -// accounts = accounts.value, -// selectedAccount = selectedAccount.value -// ) -// -// TestIdlingResource.decrement() -// } -// } -// -// private suspend fun initialiseAccounts() { -// val accounts = accountsAct(Unit) -// _accounts.value = accounts -// _selectedAccount.value = defaultAccountId(accounts) -// _selectedAccount.value?.let { -// _baseCurrencyCode.value = it.currency ?: defaultCurrencyCode -// } -// } -// -// fun createLoan(data: CreateLoanData) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// val uuid = loanCreator.create(data) { -// start() -// } -// -// uuid?.let { -// loanTransactionsLogic.Loan.createAssociatedLoanTransaction(data = data, loanId = it) -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun reorder(newOrder: List) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// ioThread { -// newOrder.forEachIndexed { index, item -> -// loanDao.save( -// item.loan.toEntity().copy( -// orderNum = index.toDouble(), -// isSynced = false -// ) -// ) -// } -// } -// start() -// -// ioThread { -// loanSync.sync() -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun createAccount(data: CreateAccountData) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// accountCreator.createAccount(data) { -// EventBus.getDefault().post(AccountsUpdatedEvent()) -// _accounts.value = accountsAct(Unit) -// _state.value = state.value.copy(accounts = _accounts.value) -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// private fun defaultAccountId( -// accounts: List, -// ): AccountOld? { -// -// val lastSelectedId = -// sharedPrefs.getString(SharedPrefs.LAST_SELECTED_ACCOUNT_ID, null)?.let { -// UUID.fromString(it) -// } -// -// lastSelectedId?.let { uuid -> -// return accounts.find { it.id == uuid } -// } ?: run { -// return if (accounts.isNotEmpty()) accounts[0] else null -// } -// } -// -// private fun findCurrencyCode(accounts: List, accountId: UUID?): String { -// return accountId?.let { -// accounts.find { account -> account.id == it }?.currency -// } ?: defaultCurrencyCode -// } -// -// private suspend fun calculateAmountPaid(loan: Loan): Double { -// val loanRecords = ioThread { loanRecordDao.findAllByLoanId(loanId = loan.id) } -// var amount = 0.0 -// -// loanRecords.forEach { loanRecord -> -// if (!loanRecord.interest) { -// val convertedAmount = loanRecord.convertedAmount ?: loanRecord.amount -// amount += convertedAmount -// } -// } -// -// return amount -// } -// -// fun onEvent(event: LoanScreenEvent) { -// viewModelScope.launch(Dispatchers.Default) { -// when (event) { -// is LoanScreenEvent.OnLoanCreate -> { -// createLoan(event.createLoanData) -// } -// is LoanScreenEvent.OnAddLoan -> { -// _state.value = _state.value.copy( -// loanModalData = LoanModalData( -// loan = null, -// baseCurrency = baseCurrencyCode.value, -// selectedAccount = selectedAccount.value -// ) -// ) -// } -// is LoanScreenEvent.OnLoanModalDismiss -> { -// _state.value = _state.value.copy( -// loanModalData = null -// ) -// } -// is LoanScreenEvent.OnReOrderModalShow -> { -// _state.value = _state.value.copy( -// reorderModalVisible = event.show -// ) -// } -// is LoanScreenEvent.OnReordered -> { -// reorder(event.reorderedList) -// _state.value = _state.value.copy( -// loans = event.reorderedList -// ) -// } -// is LoanScreenEvent.OnCreateAccount -> { -// createAccount(event.accountData) -// } -// } -// } -// } -//} -// -//data class LoanScreenState( -// val baseCurrency: String = "", -// val loans: List = emptyList(), -// val accounts: List = emptyList(), -// val selectedAccount: AccountOld? = null, -// val loanModalData: LoanModalData? = null, -// val reorderModalVisible: Boolean = false -//) -// -//sealed class LoanScreenEvent { -// data class OnLoanCreate(val createLoanData: CreateLoanData) : LoanScreenEvent() -// data class OnReordered(val reorderedList: List) : LoanScreenEvent() -// data class OnCreateAccount(val accountData: CreateAccountData) : LoanScreenEvent() -// data class OnReOrderModalShow(val show: Boolean) : LoanScreenEvent() -// object OnAddLoan : LoanScreenEvent() -// object OnLoanModalDismiss : LoanScreenEvent() -//} \ No newline at end of file diff --git a/loans/src/main/java/com/ivy/loans/loan/LoansScreen.kt b/loans/src/main/java/com/ivy/loans/loan/LoansScreen.kt deleted file mode 100644 index 71e52e7481..0000000000 --- a/loans/src/main/java/com/ivy/loans/loan/LoansScreen.kt +++ /dev/null @@ -1,400 +0,0 @@ -//package com.ivy.wallet.ui.loan -// -//import androidx.compose.foundation.* -//import androidx.compose.foundation.layout.* -//import androidx.compose.material.Text -//import androidx.compose.runtime.Composable -//import androidx.compose.runtime.collectAsState -//import androidx.compose.runtime.getValue -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.draw.clip -//import androidx.compose.ui.graphics.Color -//import androidx.compose.ui.graphics.toArgb -//import androidx.compose.ui.platform.testTag -//import androidx.compose.ui.res.stringResource -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.text.style.TextAlign -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.compose.ui.unit.sp -//import androidx.hilt.navigation.compose.hiltViewModel -//import com.ivy.base.R -//import com.ivy.base.humanReadableType -//import com.ivy.data.getDefaultFIATCurrency -//import com.ivy.data.loan.Loan -//import com.ivy.data.loan.LoanType -//import com.ivy.design.l0_system.UI -//import com.ivy.design.l0_system.style -//import com.ivy.design.util.IvyPreview -// -// -//import com.ivy.wallet.ui.loan.data.DisplayLoan -//import com.ivy.wallet.ui.theme.* -//import com.ivy.wallet.ui.theme.components.* -//import com.ivy.wallet.ui.theme.modal.LoanModal -// -//@Composable -//fun BoxWithConstraintsScope.LoansScreen() { -// val viewModel: LoanViewModel = hiltViewModel() -// -// val state by viewModel.state.collectAsState() -// -// UI( -// onEventHandler = viewModel::onEvent, -// state = state -// ) -//} -// -//@Composable -//private fun BoxWithConstraintsScope.UI( -// onEventHandler: (LoanScreenEvent) -> Unit = {}, -// state: LoanScreenState = LoanScreenState() -//) { -// -// -// Column( -// modifier = Modifier -// .fillMaxSize() -// .systemBarsPadding() -// .verticalScroll(rememberScrollState()), -// ) { -// Spacer(Modifier.height(32.dp)) -// -// Toolbar( -// setReorderModalVisible = { -// onEventHandler.invoke(LoanScreenEvent.OnReOrderModalShow(show = it)) -// } -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// for (item in state.loans) { -// Spacer(Modifier.height(16.dp)) -// -// LoanItem( -// displayLoan = item -// ) { -//// nav.navigateTo( -//// screen = LoanDetails( -//// loanId = item.loan.id -//// ) -//// ) -// } -// } -// -// if (state.loans.isEmpty()) { -// Spacer(Modifier.weight(1f)) -// -// NoLoansEmptyState( -// emptyStateTitle = stringResource(R.string.no_loans), -// emptyStateText = stringResource(R.string.no_loans_description) -// ) -// -// Spacer(Modifier.weight(1f)) -// } -// -// Spacer(Modifier.height(150.dp)) //scroll hack -// } -// -// -// LoanBottomBar( -// onAdd = { -// onEventHandler.invoke(LoanScreenEvent.OnAddLoan) -// }, -// onClose = { -// -// }, -// ) -// -// ReorderModalSingleType( -// visible = state.reorderModalVisible, -// initialItems = state.loans, -// dismiss = { -// onEventHandler.invoke(LoanScreenEvent.OnReOrderModalShow(show = false)) -// }, -// onReordered = { -// onEventHandler.invoke(LoanScreenEvent.OnReordered(reorderedList = it)) -// } -// ) { _, item -> -// Text( -// modifier = Modifier -// .fillMaxWidth() -// .padding(end = 24.dp) -// .padding(vertical = 8.dp), -// text = item.loan.name, -// style = UI.typo.b1.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.Bold -// ) -// ) -// } -// -// LoanModal( -// accounts = state.accounts, -// onCreateAccount = { -// onEventHandler.invoke(LoanScreenEvent.OnCreateAccount(accountData = it)) -// }, -// modal = state.loanModalData, -// onCreateLoan = { -// onEventHandler.invoke(LoanScreenEvent.OnLoanCreate(createLoanData = it)) -// }, -// onEditLoan = { _, _ -> }, -// dismiss = { -// onEventHandler.invoke(LoanScreenEvent.OnLoanModalDismiss) -// }, -// ) -//} -// -//@Composable -//private fun Toolbar( -// setReorderModalVisible: (Boolean) -> Unit -//) { -// Row( -// modifier = Modifier.fillMaxWidth(), -// verticalAlignment = Alignment.CenterVertically -// ) { -// Column( -// modifier = Modifier -// .weight(1f) -// .padding(start = 24.dp, end = 16.dp) -// ) { -// Text( -// text = stringResource(R.string.loans), -// style = UI.typo.h2.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// } -// -// ReorderButton { -// setReorderModalVisible(true) -// } -// -// Spacer(Modifier.width(24.dp)) -// } -//} -// -//@Composable -//private fun LoanItem( -// displayLoan: DisplayLoan, -// onClick: () -> Unit -//) { -// val loan = displayLoan.loan -// val contrastColor = findContrastTextColor(loan.color.toComposeColor()) -// -// Column( -// modifier = Modifier -// .padding(horizontal = 16.dp) -// .fillMaxWidth() -// .clip(UI.shapes.squared) -// .border(2.dp, UI.colors.medium, UI.shapes.squared) -// .testTag("loan_item") -// .clickable( -// onClick = onClick -// ) -// ) { -// LoanHeader( -// displayLoan = displayLoan, -// contrastColor = contrastColor, -// ) -// -// Spacer(Modifier.height(12.dp)) -// -// LoanInfo( -// displayLoan = displayLoan -// ) -// -// Spacer(Modifier.height(24.dp)) -// } -//} -// -//@Composable -//private fun LoanHeader( -// displayLoan: DisplayLoan, -// contrastColor: Color, -//) { -// val loan = displayLoan.loan -// -// Column( -// modifier = Modifier -// .fillMaxWidth() -// .background(loan.color.toComposeColor(), UI.shapes.squaredTop) -// ) { -// Spacer(Modifier.height(16.dp)) -// -// Row( -// verticalAlignment = Alignment.CenterVertically -// ) { -// Spacer(Modifier.width(20.dp)) -// -// ItemIconSDefaultIcon( -// iconName = loan.icon, -// defaultIcon = R.drawable.ic_custom_loan_s, -// tint = contrastColor -// ) -// -// Spacer(Modifier.width(8.dp)) -// -// Text( -// text = loan.name, -// style = UI.typo.b1.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// Spacer(Modifier.width(8.dp)) -// -// Text( -// modifier = Modifier -// .align(Alignment.Bottom) -// .padding(bottom = 4.dp), -// text = loan.humanReadableType(), -// style = UI.typo.c.style( -// color = loan.color.toComposeColor().dynamicContrast() -// ) -// ) -// -// } -// -// Spacer(Modifier.height(4.dp)) -// -// val leftToPay = loan.amount - displayLoan.amountPaid -// BalanceRow( -// modifier = Modifier -// .align(Alignment.CenterHorizontally), -// decimalPaddingTop = 7.dp, -// spacerDecimal = 6.dp, -// textColor = contrastColor, -// currency = displayLoan.currencyCode ?: getDefaultFIATCurrency().currencyCode, -// balance = leftToPay, -// -// integerFontSize = 30.sp, -// decimalFontSize = 18.sp, -// currencyFontSize = 30.sp, -// -// currencyUpfront = false -// ) -// -// Spacer(Modifier.height(16.dp)) -// } -//} -// -//@Composable -//private fun ColumnScope.LoanInfo( -// displayLoan: DisplayLoan -//) { -// -// Text( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 24.dp), -// text = displayLoan.formattedDisplayText, -// style = UI.typoSecond.b2.style( -// fontWeight = FontWeight.Bold, -// textAlign = TextAlign.Center -// ) -// ) -// -// Spacer(Modifier.height(12.dp)) -// -// ProgressBar( -// modifier = Modifier -// .fillMaxWidth() -// .height(24.dp) -// .padding(horizontal = 24.dp), -// notFilledColor = UI.colors.medium, -// percent = displayLoan.percentPaid -// ) -//} -// -//@Composable -//private fun NoLoansEmptyState( -// modifier: Modifier = Modifier, -// emptyStateTitle: String, -// emptyStateText: String, -//) { -// Column( -// modifier = modifier.fillMaxWidth(), -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Spacer(Modifier.height(32.dp)) -// -// IvyIcon( -// icon = R.drawable.ic_custom_loan_l, -// tint = Gray -// ) -// -// Spacer(Modifier.height(24.dp)) -// -// Text( -// text = emptyStateTitle, -// style = UI.typo.b1.style( -// color = Gray, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Text( -// modifier = Modifier.padding(horizontal = 32.dp), -// text = emptyStateText, -// style = UI.typo.b2.style( -// color = Gray, -// fontWeight = FontWeight.Medium, -// textAlign = TextAlign.Center -// ) -// ) -// -// Spacer(Modifier.height(96.dp)) -// } -//} -// -//@Preview -//@Composable -//private fun Preview() { -// val state = LoanScreenState( -// loans = listOf( -// DisplayLoan( -// loan = Loan( -// name = "Loan 1", -// icon = "rocket", -// color = Red.toArgb(), -// amount = 5000.0, -// type = LoanType.BORROW -// ), -// amountPaid = 0.0, -// percentPaid = 0.4 -// ), -// DisplayLoan( -// loan = Loan( -// name = "Loan 2", -// icon = "atom", -// color = Orange.toArgb(), -// amount = 252.36, -// type = LoanType.BORROW -// ), -// amountPaid = 124.23, -// percentPaid = 0.2 -// ), -// DisplayLoan( -// loan = Loan( -// name = "Loan 3", -// icon = "bank", -// color = Blue.toArgb(), -// amount = 7000.0, -// type = LoanType.LEND -// ), -// amountPaid = 8000.0, -// percentPaid = 0.8 -// ) -// ) -// ) -// IvyPreview { -// UI( -// state = state -// ) -// } -//} \ No newline at end of file diff --git a/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoan.kt b/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoan.kt deleted file mode 100644 index 3292c4e6aa..0000000000 --- a/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoan.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.ivy.wallet.ui.loan.data - -import com.ivy.base.Reorderable -import com.ivy.data.getDefaultFIATCurrency -import com.ivy.data.loan.Loan - -data class DisplayLoan( - val loan: Loan, - val amountPaid: Double, - val currencyCode: String? = getDefaultFIATCurrency().currencyCode, - val formattedDisplayText: String = "", - val percentPaid: Double = 0.0 -) : Reorderable { - override fun getItemOrderNum(): Double { - return loan.orderNum - } - - override fun withNewOrderNum(newOrderNum: Double): Reorderable { - return this.copy( - loan = loan.copy( - orderNum = newOrderNum - ) - ) - } -} \ No newline at end of file diff --git a/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoanRecord.kt b/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoanRecord.kt deleted file mode 100644 index 206833624a..0000000000 --- a/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoanRecord.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.wallet.ui.loan.data - -import com.ivy.data.AccountOld -import com.ivy.data.loan.LoanRecord - -data class DisplayLoanRecord( - val loanRecord: LoanRecord, - val account: AccountOld? = null, - val loanRecordCurrencyCode: String = "", - val loanCurrencyCode: String = "", - val loanRecordTransaction: Boolean = false, -) diff --git a/loans/src/main/java/com/ivy/loans/loan/data/EditTransactionDisplayLoan.kt b/loans/src/main/java/com/ivy/loans/loan/data/EditTransactionDisplayLoan.kt deleted file mode 100644 index 199819b9cd..0000000000 --- a/loans/src/main/java/com/ivy/loans/loan/data/EditTransactionDisplayLoan.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.ui.loan.data - -data class EditTransactionDisplayLoan( - val isLoan: Boolean = false, - val isLoanRecord: Boolean = false, - val loanCaption: String? = null, - val loanWarningDescription: String = "" -) diff --git a/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt b/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt deleted file mode 100644 index bef0166aac..0000000000 --- a/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt +++ /dev/null @@ -1,840 +0,0 @@ -//package com.ivy.wallet.ui.loandetails -// -//import androidx.compose.foundation.background -//import androidx.compose.foundation.clickable -//import androidx.compose.foundation.layout.* -//import androidx.compose.foundation.lazy.LazyColumn -//import androidx.compose.foundation.lazy.LazyListScope -//import androidx.compose.foundation.lazy.items -//import androidx.compose.foundation.lazy.rememberLazyListState -//import androidx.compose.material.Divider -//import androidx.compose.material.Text -//import androidx.compose.runtime.* -//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.toArgb -//import androidx.compose.ui.platform.testTag -//import androidx.compose.ui.res.stringResource -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.text.style.TextAlign -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.hilt.navigation.compose.hiltViewModel -//import com.ivy.base.R -//import com.ivy.base.humanReadableType -//import com.ivy.data.AccountOld -//import com.ivy.data.IvyCurrency -//import com.ivy.data.loan.Loan -//import com.ivy.data.loan.LoanRecord -//import com.ivy.data.loan.LoanType -//import com.ivy.data.transaction.TrnTypeOld -//import com.ivy.design.l0_system.UI -//import com.ivy.design.l0_system.style -//import com.ivy.design.util.IvyPreview -// -// -//import com.ivy.old.ItemStatisticToolbar -//import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -//import com.ivy.wallet.domain.deprecated.logic.model.CreateLoanRecordData -//import com.ivy.wallet.domain.deprecated.logic.model.EditLoanRecordData -//import com.ivy.wallet.ui.component.transaction.TypeAmountCurrency -//import com.ivy.wallet.ui.loan.data.DisplayLoanRecord -//import com.ivy.wallet.ui.theme.* -//import com.ivy.wallet.ui.theme.components.* -//import com.ivy.wallet.ui.theme.modal.* -//import com.ivy.wallet.utils.* -//import java.util.* -// -//@Composable -//fun BoxWithConstraintsScope.LoanDetailsScreen() { -// val viewModel: LoanDetailsViewModel = hiltViewModel() -// -// val baseCurrency by viewModel.baseCurrency.collectAsState() -// val loan by viewModel.loan.collectAsState() -// val displayLoanRecords by viewModel.displayLoanRecords.collectAsState() -// val amountPaid by viewModel.amountPaid.collectAsState() -// val loanAmountPaid by viewModel.loanAmountPaid.collectAsState() -// val accounts by viewModel.accounts.collectAsState() -// val selectedLoanAccount by viewModel.selectedLoanAccount.collectAsState() -// val createLoanTransaction by viewModel.createLoanTransaction.collectAsState() -// -// UI( -// baseCurrency = baseCurrency, -// loan = loan, -// displayLoanRecords = displayLoanRecords, -// amountPaid = amountPaid, -// loanAmountPaid = loanAmountPaid, -// accounts = accounts, -// selectedLoanAccount = selectedLoanAccount, -// createLoanTransaction = createLoanTransaction, -// -// onEditLoan = viewModel::editLoan, -// onCreateLoanRecord = viewModel::createLoanRecord, -// onEditLoanRecord = viewModel::editLoanRecord, -// onDeleteLoanRecord = viewModel::deleteLoanRecord, -// onDeleteLoan = viewModel::deleteLoan, -// onCreateAccount = viewModel::createAccount -// ) -//} -// -//@Composable -//private fun BoxWithConstraintsScope.UI( -// baseCurrency: String, -// loan: Loan?, -// displayLoanRecords: List = emptyList(), -// amountPaid: Double, -// loanAmountPaid: Double = 0.0, -// -// accounts: List = emptyList(), -// selectedLoanAccount: AccountOld? = null, -// createLoanTransaction: Boolean = false, -// -// onCreateAccount: (CreateAccountData) -> Unit = {}, -// onEditLoan: (Loan, Boolean) -> Unit = { _, _ -> }, -// onCreateLoanRecord: (CreateLoanRecordData) -> Unit = {}, -// onEditLoanRecord: (EditLoanRecordData) -> Unit = {}, -// onDeleteLoanRecord: (LoanRecord) -> Unit = {}, -// onDeleteLoan: () -> Unit = {}, -//) { -// val itemColor = loan?.color?.toComposeColor() ?: Gray -// -// var deleteModalVisible by remember { mutableStateOf(false) } -// var loanModalData: LoanModalData? by remember { mutableStateOf(null) } -// var loanRecordModalData: LoanRecordModalData? by remember { -// mutableStateOf(null) -// } -// var waitModalVisible by remember(loan) { mutableStateOf(false) } -// -// -// Column( -// modifier = Modifier -// .fillMaxSize() -// .background(itemColor) -// ) { -// val listState = rememberLazyListState() -// -// LazyColumn( -// modifier = Modifier -// .fillMaxSize() -// .statusBarsPadding() -// .padding(top = 16.dp) -// .clip(UI.shapes.roundedTop) -// .background(UI.colors.pure), -// state = listState, -// ) { -// item { -// if (loan != null) { -// Header( -// loan = loan, -// baseCurrency = baseCurrency, -// amountPaid = amountPaid, -// loanAmountPaid = loanAmountPaid, -// itemColor = itemColor, -// selectedLoanAccount = selectedLoanAccount, -// onAmountClick = { -// loanModalData = LoanModalData( -// loan = loan, -// baseCurrency = baseCurrency, -// autoFocusKeyboard = false, -// autoOpenAmountModal = true, -// selectedAccount = selectedLoanAccount, -// createLoanTransaction = createLoanTransaction -// ) -// }, -// onDeleteLoan = { -// deleteModalVisible = true -// }, -// onEditLoan = { -// loanModalData = LoanModalData( -// loan = loan, -// baseCurrency = baseCurrency, -// autoFocusKeyboard = false, -// selectedAccount = selectedLoanAccount, -// createLoanTransaction = createLoanTransaction -// ) -// }, -// onAddRecord = { -// loanRecordModalData = LoanRecordModalData( -// loanRecord = null, -// baseCurrency = baseCurrency, -// selectedAccount = selectedLoanAccount -// ) -// } -// ) -// } -// } -// -// item { -// //Rounded corners top effect -// Spacer( -// Modifier -// .height(32.dp) -// .fillMaxWidth() -// .background(itemColor) //itemColor is displayed below the clip -// .background(UI.colors.pure, UI.shapes.roundedTop) -// ) -// } -// -// if (loan != null) { -// loanRecords( -// loan = loan, -// displayLoanRecords = displayLoanRecords, -// onClick = { displayLoanRecord -> -// loanRecordModalData = LoanRecordModalData( -// loanRecord = displayLoanRecord.loanRecord, -// baseCurrency = displayLoanRecord.loanRecordCurrencyCode, -// selectedAccount = displayLoanRecord.account, -// createLoanRecordTransaction = displayLoanRecord.loanRecordTransaction, -// isLoanInterest = displayLoanRecord.loanRecord.interest, -// loanAccountCurrencyCode = displayLoanRecord.loanCurrencyCode -// ) -// } -// ) -// } -// -// if (displayLoanRecords.isEmpty()) { -// item { -// NoLoanRecordsEmptyState() -// } -// } -// -// item { -// //scroll hack -// Spacer(Modifier.height(96.dp)) -// } -// } -// } -// -// LoanModal( -// modal = loanModalData, -// onCreateLoan = { -// //do nothing -// }, -// onEditLoan = onEditLoan, -// dismiss = { -// loanModalData = null -// }, -// onCreateAccount = onCreateAccount, -// accounts = accounts, -// onPerformCalculations = { -// waitModalVisible = true -// } -// ) -// -// LoanRecordModal( -// modal = loanRecordModalData, -// onCreate = onCreateLoanRecord, -// onEdit = onEditLoanRecord, -// onDelete = onDeleteLoanRecord, -// accounts = accounts, -// dismiss = { -// loanRecordModalData = null -// }, -// onCreateAccount = onCreateAccount -// ) -// -// DeleteModal( -// visible = deleteModalVisible, -// title = stringResource(R.string.confirm_deletion), -// description = stringResource(R.string.loan_confirm_deletion_description), -// dismiss = { deleteModalVisible = false } -// ) { -// onDeleteLoan() -// } -// -// ProgressModal( -// title = stringResource(R.string.confirm_account_change), -// description = stringResource(R.string.confirm_account_loan_change), -// visible = waitModalVisible -// ) -//} -// -//@Composable -//private fun Header( -// loan: Loan, -// baseCurrency: String, -// amountPaid: Double, -// loanAmountPaid: Double = 0.0, -// itemColor: Color, -// selectedLoanAccount: AccountOld? = null, -// -// onAmountClick: () -> Unit, -// onEditLoan: () -> Unit, -// onDeleteLoan: () -> Unit, -// onAddRecord: () -> Unit -//) { -// val contrastColor = findContrastTextColor(itemColor) -// -// val darkColor = isDarkColor(itemColor) -// setStatusBarDarkTextCompat(darkText = !darkColor) -// -// Column( -// modifier = Modifier.background(itemColor) -// ) { -// Spacer(Modifier.height(20.dp)) -// -// ItemStatisticToolbar( -// contrastColor = contrastColor, -// onEdit = onEditLoan, -// onDelete = onDeleteLoan -// ) -// -// Spacer(Modifier.height(24.dp)) -// -// LoanItem( -// loan = loan, -// contrastColor = contrastColor, -// ) { -// onEditLoan() -// } -// -// BalanceRow( -// modifier = Modifier -// .padding(start = 32.dp) -// .testTag("loan_amount") -// .clickableNoIndication { -// onAmountClick() -// }, -// textColor = contrastColor, -// currency = baseCurrency, -// balance = loan.amount, -// ) -// -// -// Spacer(Modifier.height(20.dp)) -// -// LoanInfoCard( -// loan = loan, -// baseCurrency = baseCurrency, -// amountPaid = amountPaid, -// loanAmountPaid = loanAmountPaid, -// selectedLoanAccount = selectedLoanAccount, -// onAddRecord = onAddRecord -// ) -// -// Spacer(Modifier.height(20.dp)) -// } -//} -// -//@Composable -//private fun LoanItem( -// loan: Loan, -// contrastColor: Color, -// -// onClick: () -> Unit, -//) { -// Row( -// modifier = Modifier -// .padding(start = 22.dp) -// .clickableNoIndication { -// onClick() -// }, -// verticalAlignment = Alignment.CenterVertically -// ) { -// ItemIconMDefaultIcon( -// iconName = loan.icon, -// defaultIcon = R.drawable.ic_custom_loan_m, -// tint = contrastColor -// ) -// -// Spacer(Modifier.width(8.dp)) -// -// Text( -// modifier = Modifier.testTag("loan_name"), -// text = loan.name, -// style = UI.typo.b1.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.width(8.dp)) -// -// Text( -// modifier = Modifier -// .align(Alignment.Bottom) -// .padding(bottom = 12.dp), -// text = loan.humanReadableType(), -// style = UI.typo.c.style( -// color = loan.color.toComposeColor().dynamicContrast() -// ) -// ) -// } -//} -// -//@Composable -//private fun LoanInfoCard( -// loan: Loan, -// baseCurrency: String, -// amountPaid: Double, -// loanAmountPaid: Double = 0.0, -// selectedLoanAccount: AccountOld? = null, -// -// onAddRecord: () -> Unit -//) { -// val backgroundColor = if (isDarkColor(loan.color)) -// MediumBlack.copy(alpha = 0.9f) else MediumWhite.copy(alpha = 0.9f) -// -// val contrastColor = findContrastTextColor(backgroundColor) -// val percentPaid = amountPaid / loan.amount -// val loanPercentPaid = loanAmountPaid / loan.amount -// -// -// Column( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 24.dp) -// .drawColoredShadow( -// color = backgroundColor, -// alpha = 0.1f -// ) -// .background(backgroundColor, UI.shapes.rounded), -// ) { -// Row( -// verticalAlignment = Alignment.CenterVertically, -// horizontalArrangement = Arrangement.SpaceBetween, -// modifier = Modifier.fillMaxWidth() -// ) { -// Text( -// modifier = Modifier.padding(top = 8.dp, start = 24.dp), -// text = stringResource(R.string.paid), -// style = UI.typo.c.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// if (selectedLoanAccount != null) -// IvyButton( -// modifier = Modifier.padding(end = 16.dp, top = 12.dp), -// backgroundGradient = Gradient.solid(loan.color.toComposeColor()), -// hasGlow = false, -// iconTint = contrastColor, -// text = selectedLoanAccount.name, -// iconStart = getCustomIconIdS( -// iconName = selectedLoanAccount.icon, -// defaultIcon = R.drawable.ic_custom_account_s -// ), -// textStyle = UI.typo.c.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ), -// padding = 8.dp, -// iconEdgePadding = 10.dp -// ) { -//// nav.navigateTo( -//// ItemStatistic( -//// accountId = selectedLoanAccount.id, -//// categoryId = null -//// ) -//// ) -// } -// } -// -// //Support UI for Old Versions where -// if (selectedLoanAccount == null) -// Spacer(Modifier.height(12.dp)) -// -// Text( -// modifier = Modifier -// .padding(horizontal = 24.dp) -// .testTag("amount_paid"), -// text = "${amountPaid.format(baseCurrency)} / ${loan.amount.format(baseCurrency)}", -// style = UI.typoSecond.b1.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// Text( -// modifier = Modifier.padding(horizontal = 24.dp), -// text = IvyCurrency.fromCode(baseCurrency)?.name ?: "", -// style = UI.typo.b2.style( -// color = contrastColor, -// fontWeight = FontWeight.Normal -// ) -// ) -// -// Spacer(Modifier.height(12.dp)) -// -// val leftToPay = loan.amount - amountPaid -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 24.dp), -// verticalAlignment = Alignment.CenterVertically, -// ) { -// Text( -// modifier = Modifier -// .testTag("percent_paid"), -// text = "${percentPaid.times(100).format(2)}%", -// style = UI.typoSecond.b1.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.width(8.dp)) -// -// Text( -// modifier = Modifier -// .testTag("left_to_pay"), -// text = stringResource( -// R.string.left_to_pay, -// leftToPay.format(baseCurrency), -// baseCurrency -// ), -// style = UI.typoSecond.b2.style( -// color = Gray, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// } -// -// Spacer(Modifier.height(8.dp)) -// -// ProgressBar( -// modifier = Modifier -// .fillMaxWidth() -// .height(24.dp) -// .padding(horizontal = 24.dp), -// notFilledColor = UI.colors.pure, -// percent = percentPaid -// ) -// -// if (loanAmountPaid != 0.0) { -// -// Divider( -// modifier = Modifier -// .padding(horizontal = 24.dp, vertical = 16.dp) -// .height(1.dp) -// .fillMaxWidth() -// .background(contrastColor) -// ) -// -// Text( -// modifier = Modifier.padding(horizontal = 24.dp), -// text = stringResource(R.string.loan_interest), -// style = UI.typo.c.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 24.dp), -// verticalAlignment = Alignment.CenterVertically, -// ) { -// Text( -// modifier = Modifier -// .testTag("loan_interest_percent_paid"), -// text = "${loanPercentPaid.times(100).format(2)}%", -// style = UI.typoSecond.b1.style( -// color = contrastColor, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.width(8.dp)) -// -// Text( -// modifier = Modifier -// .testTag("interest_paid"), -// text = stringResource( -// R.string.interest_paid, -// loanAmountPaid.format(baseCurrency), -// baseCurrency -// ), -// style = UI.typoSecond.b2.style( -// color = Gray, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// } -// -// Spacer(Modifier.height(12.dp)) -// -// ProgressBar( -// modifier = Modifier -// .fillMaxWidth() -// .height(24.dp) -// .padding(horizontal = 24.dp), -// notFilledColor = UI.colors.pure, -// percent = loanPercentPaid -// ) -// } -// -// Spacer(Modifier.height(24.dp)) -// -// IvyButton( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 16.dp) -// .align(Alignment.CenterHorizontally), -// text = stringResource(R.string.add_record), -// shadowAlpha = 0.1f, -// backgroundGradient = Gradient.solid(contrastColor), -// textStyle = UI.typo.b2.style( -// color = findContrastTextColor(contrastColor), -// fontWeight = FontWeight.Bold -// ), -// wrapContentMode = false -// ) { -// onAddRecord() -// } -// -// Spacer(Modifier.height(12.dp)) -// } -//} -// -//fun LazyListScope.loanRecords( -// loanRecords: List = emptyList(), -// baseCurrency: String = "", -// loan: Loan, -// displayLoanRecords: List = emptyList(), -// -// onClick: (DisplayLoanRecord) -> Unit -//) { -// items(items = displayLoanRecords) { displayLoanRecord -> -// LoanRecordItem( -// loan = loan, -// loanRecord = displayLoanRecord.loanRecord, -// baseCurrency = displayLoanRecord.loanRecordCurrencyCode, -// account = displayLoanRecord.account, -// loanBaseCurrency = displayLoanRecord.loanCurrencyCode -// ) { -// onClick(displayLoanRecord) -// } -// -// Spacer(modifier = Modifier.height(16.dp)) -// } -//} -// -//@Composable -//private fun LoanRecordItem( -// loan: Loan, -// loanRecord: LoanRecord, -// baseCurrency: String, -// loanBaseCurrency: String = "", -// account: AccountOld? = null, -// onClick: () -> Unit -//) { -// -// Column( -// modifier = Modifier -// .fillMaxWidth() -// .padding(horizontal = 16.dp) -// .clip(UI.shapes.squared) -// .clickable { -// onClick() -// } -// .background(UI.colors.medium, UI.shapes.squared) -// .testTag("loan_record_item") -// ) { -// -// if (account != null || loanRecord.interest) { -// Row(Modifier.padding(16.dp)) { -// if (account != null) { -// IvyButton( -// backgroundGradient = Gradient.solid(UI.colors.pure), -// hasGlow = false, -// iconTint = UI.colorsInverted.pure, -// text = account.name, -// iconStart = getCustomIconIdS( -// iconName = account.icon, -// defaultIcon = R.drawable.ic_custom_account_s -// ), -// textStyle = UI.typo.c.style( -// color = UI.colorsInverted.pure, -// fontWeight = FontWeight.ExtraBold -// ), -// padding = 8.dp, -// iconEdgePadding = 10.dp -// ) { -//// nav.navigateTo( -//// ItemStatistic( -//// accountId = account.id, -//// categoryId = null -//// ) -//// ) -// } -// } -// -// if (loanRecord.interest) { -// //Spacer(modifier = Modifier.width(8.dp)) -// -// val textIconColor = if (isDarkColor(loan.color)) MediumWhite else MediumBlack -// -// IvyButton( -// modifier = Modifier.padding(start = 8.dp), -// backgroundGradient = Gradient.solid(loan.color.toComposeColor()), -// hasGlow = false, -// iconTint = textIconColor, -// text = stringResource(R.string.interest), -// iconStart = getCustomIconIdS( -// iconName = "currency", -// defaultIcon = R.drawable.ic_currency -// ), -// textStyle = UI.typo.c.style( -// color = textIconColor, -// fontWeight = FontWeight.ExtraBold -// ), -// padding = 8.dp, -// iconEdgePadding = 10.dp -// ) { -// //do Nothing -// } -// } -// } -// } else { -// Spacer(Modifier.height(20.dp)) -// } -// -// Text( -// modifier = Modifier.padding(horizontal = 24.dp), -// text = loanRecord.dateTime.formatNicelyWithTime( -// noWeekDay = false -// ).uppercase(), -// style = UI.typoSecond.c.style( -// color = Gray, -// fontWeight = FontWeight.Bold -// ) -// ) -// -// if (loanRecord.note.isNotNullOrBlank()) { -// Text( -// modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), -// text = loanRecord.note!!, -// style = UI.typo.b1.style( -// fontWeight = FontWeight.ExtraBold, -// color = UI.colorsInverted.pure -// ) -// ) -// } -// -// if (loanRecord.note.isNullOrEmpty()) -// Spacer(Modifier.height(16.dp)) -// -// TypeAmountCurrency( -// transactionType = if (loan.type == LoanType.LEND) TrnTypeOld.INCOME else TrnTypeOld.EXPENSE, -// dueDate = null, -// currency = baseCurrency, -// amount = loanRecord.amount -// ) -// -// if (loanRecord.convertedAmount != null) { -// Text( -// modifier = Modifier.padding(start = 68.dp), -// text = loanRecord.convertedAmount!!.format(baseCurrency) + " $loanBaseCurrency", -// style = UI.typoSecond.b2.style( -// color = Gray, -// fontWeight = FontWeight.Normal -// ) -// ) -// } -// -// Spacer(Modifier.height(16.dp)) -// } -//} -// -//@Composable -//private fun NoLoanRecordsEmptyState() { -// Column( -// modifier = Modifier.fillMaxWidth(), -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Spacer(Modifier.height(32.dp)) -// -// IvyIcon( -// icon = R.drawable.ic_notransactions, -// tint = Gray -// ) -// -// Spacer(Modifier.height(24.dp)) -// -// Text( -// text = stringResource(R.string.no_records), -// style = UI.typo.b1.style( -// color = Gray, -// fontWeight = FontWeight.ExtraBold -// ) -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Text( -// modifier = Modifier.padding(horizontal = 32.dp), -// text = stringResource(R.string.no_records_for_the_loan), -// style = UI.typo.b2.style( -// color = Gray, -// fontWeight = FontWeight.Medium, -// textAlign = TextAlign.Center -// ) -// ) -// } -// -// Spacer(Modifier.height(96.dp)) -//} -// -//@Preview -//@Composable -//private fun Preview_Empty() { -// IvyPreview { -// UI( -// baseCurrency = "BGN", -// loan = Loan( -// name = "Loan 1", -// amount = 4023.54, -// color = Red.toArgb(), -// type = LoanType.LEND -// ), -// amountPaid = 0.0 -// ) -// } -//} -// -//@Preview -//@Composable -//private fun Preview_Records() { -// IvyPreview { -// UI( -// baseCurrency = "BGN", -// loan = Loan( -// name = "Loan 1", -// amount = 4023.54, -// color = Red.toArgb(), -// type = LoanType.LEND -// ), -// displayLoanRecords = listOf( -// DisplayLoanRecord( -// LoanRecord( -// amount = 123.45, -// dateTime = timeNowUTC().minusDays(1), -// note = "Cash", -// loanId = UUID.randomUUID() -// ) -// ), -// DisplayLoanRecord( -// LoanRecord( -// amount = 0.50, -// dateTime = timeNowUTC().minusYears(1), -// loanId = UUID.randomUUID() -// ) -// ), -// DisplayLoanRecord( -// LoanRecord( -// amount = 1000.00, -// dateTime = timeNowUTC().minusMonths(1), -// note = "Revolut", -// loanId = UUID.randomUUID() -// ) -// ), -// ), -// amountPaid = 3821.00 -// ) -// } -//} \ No newline at end of file diff --git a/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt b/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt deleted file mode 100644 index 6bc6f64abc..0000000000 --- a/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt +++ /dev/null @@ -1,312 +0,0 @@ -//package com.ivy.wallet.ui.loandetails -// -//import androidx.lifecycle.ViewModel -//import androidx.lifecycle.viewModelScope -//import com.ivy.core.ui.temp.trash.IvyWalletCtx -//import com.ivy.data.AccountOld -//import com.ivy.data.loan.Loan -//import com.ivy.data.loan.LoanRecord -//import com.ivy.data.transaction.TransactionOld -//import com.ivy.frp.test.TestIdlingResource -// -//import com.ivy.temp.event.AccountsUpdatedEvent -//import com.ivy.wallet.domain.action.account.AccountsActOld -//import com.ivy.wallet.domain.action.loan.LoanByIdAct -//import com.ivy.wallet.domain.deprecated.logic.loantrasactions.LoanTransactionsLogic -//import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -//import com.ivy.wallet.domain.deprecated.logic.model.CreateLoanRecordData -//import com.ivy.wallet.domain.deprecated.logic.model.EditLoanRecordData -//import com.ivy.wallet.io.persistence.dao.* -//import com.ivy.wallet.ui.loan.data.DisplayLoanRecord -//import com.ivy.wallet.utils.computationThread -//import com.ivy.wallet.utils.ioThread -//import dagger.hilt.android.lifecycle.HiltViewModel -//import kotlinx.coroutines.flow.MutableStateFlow -//import kotlinx.coroutines.flow.asStateFlow -//import kotlinx.coroutines.launch -//import org.greenrobot.eventbus.EventBus -//import java.util.* -//import javax.inject.Inject -// -//@HiltViewModel -//class LoanDetailsViewModel @Inject constructor( -// private val loanDao: LoanDao, -// private val loanRecordDao: LoanRecordDao, -// private val loanCreator: LoanCreator, -// private val loanRecordCreator: LoanRecordCreator, -// private val settingsDao: SettingsDao, -// private val ivyContext: IvyWalletCtx, -// private val transactionDao: TransactionDao, -// private val accountDao: AccountDao, -// private val accountCreator: AccountCreator, -// private val loanTransactionsLogic: LoanTransactionsLogic, -// private val -// private val accountsAct: AccountsActOld, -// private val loanByIdAct: LoanByIdAct -//) : ViewModel() { -// -// private val _baseCurrency = MutableStateFlow("") -// val baseCurrency = _baseCurrency.asStateFlow() -// -// private val _loan = MutableStateFlow(null) -// val loan = _loan.asStateFlow() -// -// private val _displayLoanRecords = MutableStateFlow(emptyList()) -// val displayLoanRecords = _displayLoanRecords.asStateFlow() -// -// private val _amountPaid = MutableStateFlow(0.0) -// val amountPaid = _amountPaid.asStateFlow() -// -// private val _accounts = MutableStateFlow>(emptyList()) -// val accounts = _accounts.asStateFlow() -// -// private val _loanInterestAmountPaid = MutableStateFlow(0.0) -// val loanAmountPaid = _loanInterestAmountPaid.asStateFlow() -// -// private val _selectedLoanAccount = MutableStateFlow(null) -// val selectedLoanAccount = _selectedLoanAccount.asStateFlow() -// -// private var associatedTransaction: TransactionOld? = null -// -// private val _createLoanTransaction = MutableStateFlow(false) -// val createLoanTransaction = _createLoanTransaction.asStateFlow() -// -// private var defaultCurrencyCode = "" -// -// fun start() { -//// load(loanId = screen.loanId) -// } -// -// private fun load(loanId: UUID) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// defaultCurrencyCode = ioThread { -// settingsDao.findFirstSuspend().currency -// }.also { -// _baseCurrency.value = it -// } -// -// _accounts.value = accountsAct(Unit) -// -// _loan.value = loanByIdAct(loanId) -// -// loan.value?.let { loan -> -// _selectedLoanAccount.value = accounts.value.find { -// loan.accountId == it.id -// } -// -// _selectedLoanAccount.value?.let { acc -> -// _baseCurrency.value = acc.currency ?: defaultCurrencyCode -// } -// } -// -// computationThread { -// _displayLoanRecords.value = -// ioThread { loanRecordDao.findAllByLoanId(loanId = loanId) }.map { -// val trans = ioThread { -// transactionDao.findLoanRecordTransaction( -// it.id -// ) -// } -// -// val account = findAccount( -// accounts = accounts.value, -// accountId = it.accountId, -// ) -// -// DisplayLoanRecord( -// it.toDomain(), -// account = account, -// loanRecordTransaction = trans != null, -// loanRecordCurrencyCode = account?.currency ?: defaultCurrencyCode, -// loanCurrencyCode = selectedLoanAccount.value?.currency -// ?: defaultCurrencyCode -// ) -// } -// } -// -// computationThread { -// //Using a local variable to calculate the amount and then reassigning to -// // the State variable to reduce the amount of compose re-draws -// var amtPaid = 0.0 -// var loanInterestAmtPaid = 0.0 -// displayLoanRecords.value.forEach { -// val convertedAmount = it.loanRecord.convertedAmount ?: it.loanRecord.amount -// if (!it.loanRecord.interest) { -// amtPaid += convertedAmount -// } else -// loanInterestAmtPaid += convertedAmount -// } -// -// _amountPaid.value = amtPaid -// _loanInterestAmountPaid.value = loanInterestAmtPaid -// } -// -// associatedTransaction = ioThread { -// transactionDao.findLoanTransaction(loanId = loan.value!!.id)?.toDomain() -// } -// -// associatedTransaction?.let { -// _createLoanTransaction.value = true -// } ?: run { -// _createLoanTransaction.value = false -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun editLoan(loan: Loan, createLoanTransaction: Boolean = false) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// _loan.value?.let { -// loanTransactionsLogic.Loan.recalculateLoanRecords( -// oldLoanAccountId = it.accountId, -// newLoanAccountId = loan.accountId, -// loanId = loan.id -// ) -// } -// -// loanTransactionsLogic.Loan.editAssociatedLoanTransaction( -// loan = loan, -// createLoanTransaction = createLoanTransaction, -// transaction = associatedTransaction -// ) -// -// loanCreator.edit(loan) { -// load(loanId = it.id) -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun deleteLoan() { -// val loan = loan.value ?: return -// -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// loanTransactionsLogic.Loan.deleteAssociatedLoanTransactions(loan.id) -// -// loanCreator.delete(loan) { -// //close screen -// -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun createLoanRecord(data: CreateLoanRecordData) { -// if (loan.value == null) return -// val loanId = loan.value?.id ?: return -// val localLoan = loan.value!! -// -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// val modifiedData = data.copy( -// convertedAmount = loanTransactionsLogic.LoanRecord.calculateConvertedAmount( -// data = data, -// loanAccountId = localLoan.accountId -// ) -// ) -// -// val loanRecordUUID = loanRecordCreator.create( -// loanId = loanId, -// data = modifiedData -// ) { -// load(loanId = loanId) -// } -// -// loanRecordUUID?.let { -// loanTransactionsLogic.LoanRecord.createAssociatedLoanRecordTransaction( -// data = modifiedData, -// loan = localLoan, -// loanRecordId = it -// ) -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun editLoanRecord(editLoanRecordData: EditLoanRecordData) { -// viewModelScope.launch { -// val loanRecord = editLoanRecordData.newLoanRecord -// TestIdlingResource.increment() -// -// val localLoan: Loan = _loan.value ?: return@launch -// -// val convertedAmount = -// loanTransactionsLogic.LoanRecord.calculateConvertedAmount( -// loanAccountId = localLoan.accountId, -// newLoanRecord = editLoanRecordData.newLoanRecord, -// oldLoanRecord = editLoanRecordData.originalLoanRecord, -// reCalculateLoanAmount = editLoanRecordData.reCalculateLoanAmount -// ) -// -// val modifiedLoanRecord = -// editLoanRecordData.newLoanRecord.copy(convertedAmount = convertedAmount) -// -// loanTransactionsLogic.LoanRecord.editAssociatedLoanRecordTransaction( -// loan = localLoan, -// createLoanRecordTransaction = editLoanRecordData.createLoanRecordTransaction, -// loanRecord = loanRecord, -// ) -// -// loanRecordCreator.edit(modifiedLoanRecord) { -// load(loanId = it.loanId) -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// fun deleteLoanRecord(loanRecord: LoanRecord) { -// val loanId = loan.value?.id ?: return -// -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// loanRecordCreator.delete(loanRecord) { -// load(loanId = loanId) -// } -// -// loanTransactionsLogic.LoanRecord.deleteAssociatedLoanRecordTransaction(loanRecordId = loanRecord.id) -// -// TestIdlingResource.decrement() -// } -// } -// -// fun onLoanTransactionChecked(boolean: Boolean) { -// _createLoanTransaction.value = boolean -// } -// -// fun createAccount(data: CreateAccountData) { -// viewModelScope.launch { -// TestIdlingResource.increment() -// -// accountCreator.createAccount(data) { -// EventBus.getDefault().post(AccountsUpdatedEvent()) -// _accounts.value = accountsAct(Unit) -// } -// -// TestIdlingResource.decrement() -// } -// } -// -// private fun findAccount( -// accounts: List, -// accountId: UUID?, -// ): AccountOld? { -// return accountId?.let { uuid -> -// accounts.find { acc -> -// acc.id == uuid -// } -// } -// } -//} \ No newline at end of file diff --git a/main/README.md b/main/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/main/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/import-csv-backup/.gitignore b/main/base/.gitignore similarity index 100% rename from import-csv-backup/.gitignore rename to main/base/.gitignore diff --git a/main/base/build.gradle.kts b/main/base/build.gradle.kts new file mode 100644 index 0000000000..1229a440e4 --- /dev/null +++ b/main/base/build.gradle.kts @@ -0,0 +1,14 @@ +import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing + +apply() + +plugins { + `android-library` + `kotlin-android` +} + +dependencies { + Hilt() + Testing() +} \ No newline at end of file diff --git a/main/base/src/main/AndroidManifest.xml b/main/base/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..caadc33601 --- /dev/null +++ b/main/base/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/accounts/src/main/java/com/ivy/accounts/AccBottomBarAction.kt b/main/base/src/main/java/com/ivy/main/base/MainBottomBarAction.kt similarity index 50% rename from accounts/src/main/java/com/ivy/accounts/AccBottomBarAction.kt rename to main/base/src/main/java/com/ivy/main/base/MainBottomBarAction.kt index ac7333507e..30870a11b5 100644 --- a/accounts/src/main/java/com/ivy/accounts/AccBottomBarAction.kt +++ b/main/base/src/main/java/com/ivy/main/base/MainBottomBarAction.kt @@ -1,5 +1,5 @@ -package com.ivy.accounts +package com.ivy.main.base -enum class AccBottomBarAction { +enum class MainBottomBarAction { Click, SwipeUp, SwipeDiagonalLeft, SwipeDiagonalRight } \ No newline at end of file diff --git a/main/base/src/main/java/com/ivy/main/base/MainBottomBarVisibility.kt b/main/base/src/main/java/com/ivy/main/base/MainBottomBarVisibility.kt new file mode 100644 index 0000000000..aaa8e3487c --- /dev/null +++ b/main/base/src/main/java/com/ivy/main/base/MainBottomBarVisibility.kt @@ -0,0 +1,10 @@ +package com.ivy.main.base + +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MainBottomBarVisibility @Inject constructor() { + val visible = MutableStateFlow(false) +} \ No newline at end of file diff --git a/item-transactions/.gitignore b/main/impl/.gitignore similarity index 100% rename from item-transactions/.gitignore rename to main/impl/.gitignore diff --git a/balance-prediction/README.md b/main/impl/README.md similarity index 100% rename from balance-prediction/README.md rename to main/impl/README.md diff --git a/main/build.gradle.kts b/main/impl/build.gradle.kts similarity index 92% rename from main/build.gradle.kts rename to main/impl/build.gradle.kts index 530b249b23..2f340c3cc9 100644 --- a/main/build.gradle.kts +++ b/main/impl/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(project(":core:domain")) implementation(project(":navigation")) + implementation(project(":main:base")) implementation(project(":home:tab")) implementation(project(":accounts")) } \ No newline at end of file diff --git a/main/impl/src/main/AndroidManifest.xml b/main/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b706539b8a --- /dev/null +++ b/main/impl/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/main/src/main/java/com/ivy/main/MainEvent.kt b/main/impl/src/main/java/com/ivy/main/impl/MainEvent.kt similarity index 87% rename from main/src/main/java/com/ivy/main/MainEvent.kt rename to main/impl/src/main/java/com/ivy/main/impl/MainEvent.kt index b3adefd098..81a1c5cc65 100644 --- a/main/src/main/java/com/ivy/main/MainEvent.kt +++ b/main/impl/src/main/java/com/ivy/main/impl/MainEvent.kt @@ -1,4 +1,4 @@ -package com.ivy.main +package com.ivy.main.impl import com.ivy.navigation.destinations.main.Main diff --git a/main/src/main/java/com/ivy/main/MainScreen.kt b/main/impl/src/main/java/com/ivy/main/impl/MainScreen.kt similarity index 68% rename from main/src/main/java/com/ivy/main/MainScreen.kt rename to main/impl/src/main/java/com/ivy/main/impl/MainScreen.kt index a3aac76458..8c68757288 100644 --- a/main/src/main/java/com/ivy/main/MainScreen.kt +++ b/main/impl/src/main/java/com/ivy/main/impl/MainScreen.kt @@ -1,7 +1,6 @@ -package com.ivy.main +package com.ivy.main.impl -import AccountTab import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -12,17 +11,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.accounts.AccBottomBarAction -import com.ivy.accounts.AccountEvent -import com.ivy.accounts.AccountViewModel +import com.ivy.accounts.AccountTab +import com.ivy.accounts.AccountTabEvent +import com.ivy.accounts.AccountTabViewModel +import com.ivy.design.l2_components.modal.openModals import com.ivy.design.util.IvyPreview import com.ivy.design.util.ScreenPlaceholder import com.ivy.design.util.isInPreview +import com.ivy.home.HomeEvent import com.ivy.home.HomeTab import com.ivy.home.HomeViewModel -import com.ivy.home.event.HomeBottomBarAction -import com.ivy.home.event.HomeEvent -import com.ivy.main.components.BottomBar +import com.ivy.main.base.MainBottomBarAction +import com.ivy.main.impl.components.MainBottomBar import com.ivy.navigation.destinations.main.Main.Tab import com.ivy.wallet.utils.horizontalSwipeListener @@ -35,9 +35,10 @@ fun MainScreen(tab: Tab?) { } val homeViewModel: HomeViewModel = hiltViewModel() - val accountViewModel: AccountViewModel = hiltViewModel() + val accountViewModel: AccountTabViewModel = hiltViewModel() UI( selectedTab = state.selectedTab, + bottomBarVisible = state.bottomBarVisible, onEvent = viewModel::onEvent, onHomeTabEvent = homeViewModel::onEvent, onAccountTabEvent = accountViewModel::onEvent, @@ -47,17 +48,18 @@ fun MainScreen(tab: Tab?) { @Composable private fun UI( selectedTab: Tab, + bottomBarVisible: Boolean, onEvent: (MainEvent) -> Unit, onHomeTabEvent: (HomeEvent) -> Unit, - onAccountTabEvent: (AccountEvent) -> Unit, + onAccountTabEvent: (AccountTabEvent) -> Unit, ) { Box( modifier = Modifier .fillMaxSize() .horizontalSwipeListener( sensitivity = 200, - onSwipeLeft = { onEvent(MainEvent.SwitchSelectedTab) }, - onSwipeRight = { onEvent(MainEvent.SwitchSelectedTab) } + onSwipeLeft = { switchTabs(onEvent) }, + onSwipeRight = { switchTabs(onEvent) } ) ) { when (selectedTab) { @@ -66,7 +68,8 @@ private fun UI( } // region Bottom bar - BottomBar( + MainBottomBar( + visible = bottomBarVisible, modifier = Modifier .align(Alignment.BottomCenter) .padding(horizontal = 16.dp) @@ -75,8 +78,8 @@ private fun UI( selectedTab = selectedTab, onActionClick = { propagateBottomActionEvent( - homeEvent = homeEvent(HomeBottomBarAction.Click), - accountEvent = accountEvent(AccBottomBarAction.Click), + homeEvent = homeEvent(MainBottomBarAction.Click), + accountEvent = accountEvent(MainBottomBarAction.Click), selectedTab = selectedTab, onHomeTabEvent = onHomeTabEvent, onAccountTabEvent = onAccountTabEvent @@ -84,8 +87,8 @@ private fun UI( }, onActionSwipeUp = { propagateBottomActionEvent( - homeEvent = homeEvent(HomeBottomBarAction.SwipeUp), - accountEvent = accountEvent(AccBottomBarAction.SwipeUp), + homeEvent = homeEvent(MainBottomBarAction.SwipeUp), + accountEvent = accountEvent(MainBottomBarAction.SwipeUp), selectedTab = selectedTab, onHomeTabEvent = onHomeTabEvent, onAccountTabEvent = onAccountTabEvent @@ -93,8 +96,8 @@ private fun UI( }, onActionSwipeDiagonalLeft = { propagateBottomActionEvent( - homeEvent = homeEvent(HomeBottomBarAction.SwipeDiagonalLeft), - accountEvent = accountEvent(AccBottomBarAction.SwipeDiagonalLeft), + homeEvent = homeEvent(MainBottomBarAction.SwipeDiagonalLeft), + accountEvent = accountEvent(MainBottomBarAction.SwipeDiagonalLeft), selectedTab = selectedTab, onHomeTabEvent = onHomeTabEvent, onAccountTabEvent = onAccountTabEvent @@ -102,8 +105,8 @@ private fun UI( }, onActionSwipeDiagonalRight = { propagateBottomActionEvent( - homeEvent = homeEvent(HomeBottomBarAction.SwipeDiagonalRight), - accountEvent = accountEvent(AccBottomBarAction.SwipeDiagonalRight), + homeEvent = homeEvent(MainBottomBarAction.SwipeDiagonalRight), + accountEvent = accountEvent(MainBottomBarAction.SwipeDiagonalRight), selectedTab = selectedTab, onHomeTabEvent = onHomeTabEvent, onAccountTabEvent = onAccountTabEvent @@ -116,13 +119,19 @@ private fun UI( } } +private fun switchTabs(onEvent: (MainEvent) -> Unit) { + if (openModals <= 0) { + onEvent(MainEvent.SwitchSelectedTab) + } +} + // region Bottom Action Bar events propagation private fun propagateBottomActionEvent( homeEvent: HomeEvent, - accountEvent: AccountEvent, + accountEvent: AccountTabEvent, selectedTab: Tab, onHomeTabEvent: (HomeEvent) -> Unit, - onAccountTabEvent: (AccountEvent) -> Unit + onAccountTabEvent: (AccountTabEvent) -> Unit ) { when (selectedTab) { Tab.Home -> onHomeTabEvent(homeEvent) @@ -130,11 +139,11 @@ private fun propagateBottomActionEvent( } } -private fun homeEvent(action: HomeBottomBarAction): HomeEvent = +private fun homeEvent(action: MainBottomBarAction): HomeEvent = HomeEvent.BottomBarAction(action) -private fun accountEvent(action: AccBottomBarAction): AccountEvent = - AccountEvent.BottomBarAction(action) +private fun accountEvent(action: MainBottomBarAction): AccountTabEvent = + AccountTabEvent.BottomBarAction(action) // endregion // region Preview-safe Tabs @@ -173,6 +182,21 @@ private fun Preview() { IvyPreview { UI( selectedTab = Tab.Home, + bottomBarVisible = true, + onEvent = {}, + onHomeTabEvent = {}, + onAccountTabEvent = {} + ) + } +} + +@Preview +@Composable +private fun Preview_BottomBar_hidden() { + IvyPreview { + UI( + selectedTab = Tab.Home, + bottomBarVisible = false, onEvent = {}, onHomeTabEvent = {}, onAccountTabEvent = {} diff --git a/main/src/main/java/com/ivy/main/MainState.kt b/main/impl/src/main/java/com/ivy/main/impl/MainState.kt similarity index 57% rename from main/src/main/java/com/ivy/main/MainState.kt rename to main/impl/src/main/java/com/ivy/main/impl/MainState.kt index f213bc8c37..4402faaef7 100644 --- a/main/src/main/java/com/ivy/main/MainState.kt +++ b/main/impl/src/main/java/com/ivy/main/impl/MainState.kt @@ -1,9 +1,10 @@ -package com.ivy.main +package com.ivy.main.impl import androidx.compose.runtime.Immutable import com.ivy.navigation.destinations.main.Main @Immutable data class MainState( - val selectedTab: Main.Tab + val selectedTab: Main.Tab, + val bottomBarVisible: Boolean, ) \ No newline at end of file diff --git a/main/src/main/java/com/ivy/main/MainViewModel.kt b/main/impl/src/main/java/com/ivy/main/impl/MainViewModel.kt similarity index 52% rename from main/src/main/java/com/ivy/main/MainViewModel.kt rename to main/impl/src/main/java/com/ivy/main/impl/MainViewModel.kt index 82dd60e7ac..b853742b22 100644 --- a/main/src/main/java/com/ivy/main/MainViewModel.kt +++ b/main/impl/src/main/java/com/ivy/main/impl/MainViewModel.kt @@ -1,27 +1,36 @@ -package com.ivy.main +package com.ivy.main.impl -import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.main.base.MainBottomBarVisibility import com.ivy.navigation.destinations.main.Main.Tab import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor() : FlowViewModel() { - override fun initialState(): MainState = MainState(selectedTab = Tab.Home) - - override fun initialUiState(): MainState = initialState() +class MainViewModel @Inject constructor( + bottomBarVisibility: MainBottomBarVisibility, +) : SimpleFlowViewModel() { + override val initialUi: MainState = MainState( + selectedTab = Tab.Home, + bottomBarVisible = true, + ) private val selectedTab = MutableStateFlow(Tab.Home) - override fun stateFlow(): Flow = selectedTab.map { - MainState(selectedTab = it) + override val uiFlow: Flow = combine( + selectedTab, bottomBarVisibility.visible + ) { selectedTab, bottomBarVisible -> + MainState( + selectedTab = selectedTab, + bottomBarVisible = bottomBarVisible, + ) } - override suspend fun mapToUiState(state: MainState) = state + // region Event Handling override suspend fun handleEvent(event: MainEvent) = when (event) { is MainEvent.SelectTab -> selectTab(event) MainEvent.SwitchSelectedTab -> toggleTabs() @@ -37,9 +46,10 @@ class MainViewModel @Inject constructor() : FlowViewModel Tab.Accounts Tab.Accounts -> Tab.Home } } + // endregion } \ No newline at end of file diff --git a/main/src/main/java/com/ivy/main/components/BottomBar.kt b/main/impl/src/main/java/com/ivy/main/impl/components/MainBottomBar.kt similarity index 79% rename from main/src/main/java/com/ivy/main/components/BottomBar.kt rename to main/impl/src/main/java/com/ivy/main/impl/components/MainBottomBar.kt index ced22f985d..316b03a43f 100644 --- a/main/src/main/java/com/ivy/main/components/BottomBar.kt +++ b/main/impl/src/main/java/com/ivy/main/impl/components/MainBottomBar.kt @@ -1,7 +1,11 @@ -package com.ivy.main.components +package com.ivy.main.impl.components import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Arrangement @@ -17,31 +21,63 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.ivy.design.animation.slideInBottom +import com.ivy.design.animation.slideOutBottom import com.ivy.design.l0_system.UI import com.ivy.design.l1_buildingBlocks.B2 import com.ivy.design.l1_buildingBlocks.IconRes import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.design.l1_buildingBlocks.SpacerVer -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.ComponentPreview import com.ivy.design.util.consumeClicks -import com.ivy.main.R import com.ivy.navigation.destinations.main.Main.Tab +import com.ivy.resources.R import kotlin.math.abs @Composable -internal fun BottomBar( +internal fun MainBottomBar( + visible: Boolean, selectedTab: Tab, + modifier: Modifier = Modifier, + onActionClick: (Tab) -> Unit, + onActionSwipeUp: () -> Unit, + onActionSwipeDiagonalLeft: () -> Unit, + onActionSwipeDiagonalRight: () -> Unit, + onHomeClick: () -> Unit, + onAccountsClick: () -> Unit, +) { + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = slideInBottom() + fadeIn(), + exit = slideOutBottom() + fadeOut(), + ) { + BottomBarRow( + selectedTab = selectedTab, + onActionClick = onActionClick, + onActionSwipeUp = onActionSwipeUp, + onActionSwipeDiagonalLeft = onActionSwipeDiagonalLeft, + onActionSwipeDiagonalRight = onActionSwipeDiagonalRight, + onHomeClick = onHomeClick, + onAccountsClick = onAccountsClick + ) + } +} + +@Composable +private fun BottomBarRow( + selectedTab: Tab, + modifier: Modifier = Modifier, onActionClick: (Tab) -> Unit, onActionSwipeUp: () -> Unit, onActionSwipeDiagonalLeft: () -> Unit, onActionSwipeDiagonalRight: () -> Unit, onHomeClick: () -> Unit, onAccountsClick: () -> Unit, - modifier: Modifier = Modifier ) { Row( modifier = modifier @@ -50,6 +86,7 @@ internal fun BottomBar( color = UI.colors.medium.copy(alpha = 0.9f), shape = UI.shapes.rounded ) + .border(1.dp, UI.colors.primary, UI.shapes.rounded) .consumeClicks() .padding(horizontal = 16.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically @@ -80,6 +117,7 @@ internal fun BottomBar( } } + @Composable private fun Tab( text: String, @@ -158,8 +196,8 @@ private fun ActionButton( ) }, size = ButtonSize.Small, - visibility = ButtonVisibility.Focused, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Focused, + feeling = Feeling.Positive, text = null, icon = R.drawable.ic_round_add_24, onClick = onClick @@ -172,7 +210,8 @@ private fun ActionButton( @Composable private fun Preview_Home() { ComponentPreview { - BottomBar( + MainBottomBar( + visible = true, modifier = Modifier.padding(horizontal = 16.dp), selectedTab = Tab.Home, onActionClick = {}, @@ -189,7 +228,8 @@ private fun Preview_Home() { @Composable private fun Preview_Account() { ComponentPreview { - BottomBar( + MainBottomBar( + visible = true, modifier = Modifier.padding(horizontal = 16.dp), selectedTab = Tab.Accounts, onActionClick = {}, diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml deleted file mode 100644 index 9a8b8b1be6..0000000000 --- a/main/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/loans/.gitignore b/math/.gitignore similarity index 100% rename from loans/.gitignore rename to math/.gitignore diff --git a/math/README.md b/math/README.md new file mode 100644 index 0000000000..f93c152ff2 --- /dev/null +++ b/math/README.md @@ -0,0 +1,7 @@ +# Math + +The `:math` module is responsible for: + +- Parsing and evaluating mathematical expressions. +- Syntax checking and suggestions when typing expressions. +- Common math functions that aren't implemented in Kotlin's standard library. \ No newline at end of file diff --git a/sync/public/build.gradle.kts b/math/build.gradle.kts similarity index 57% rename from sync/public/build.gradle.kts rename to math/build.gradle.kts index daf0af41f3..2e0943dd77 100644 --- a/sync/public/build.gradle.kts +++ b/math/build.gradle.kts @@ -1,4 +1,5 @@ import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing apply() @@ -10,7 +11,6 @@ plugins { dependencies { Hilt() implementation(project(":common:main")) - implementation(project(":temp-persistence")) - implementation(project(":sync:base")) -// implementation(project(":sync:ivy-server")) + implementation(project(":parser")) + Testing() } \ No newline at end of file diff --git a/sync/public/src/main/AndroidManifest.xml b/math/src/main/AndroidManifest.xml similarity index 52% rename from sync/public/src/main/AndroidManifest.xml rename to math/src/main/AndroidManifest.xml index e32ca00af7..a9430f8821 100644 --- a/sync/public/src/main/AndroidManifest.xml +++ b/math/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/EvaluateExpression.kt b/math/src/main/java/com/ivy/math/EvaluateExpression.kt new file mode 100644 index 0000000000..0c978539c7 --- /dev/null +++ b/math/src/main/java/com/ivy/math/EvaluateExpression.kt @@ -0,0 +1,46 @@ +package com.ivy.math + +import com.ivy.math.calculator.bracketsClosed +import timber.log.Timber + +fun evaluate(expression: String): Double? { + val parser = expressionParser() + val fixedExpression = tryFixExpression(normalize(expression)) + val result = parser(fixedExpression) + val expressionTree = result.firstOrNull() + ?.takeIf { it.leftover.isEmpty() }?.value ?: return null + Timber.d("Evaluating: ${expressionTree.print()}") + return expressionTree.eval() +} + +fun tryFixExpression(expression: String): String { + fun fixPartialBinaryOps(expression: String): String = when (expression.lastOrNull()) { + '+', '-', '*', '/' -> expression.dropLast(1) + else -> when { + expression.endsWith("()") -> fixPartialBinaryOps(expression.dropLast(2)) + expression.endsWith("(") -> fixPartialBinaryOps(expression.dropLast(1)) + else -> expression + } + } + + fun fixLeadingPlus(expression: String): String = if (expression.firstOrNull() == '+') + expression.drop(1) else expression + + var fixBrackets = fixLeadingPlus(expression) + .let(::fixPartialBinaryOps) + .replace("(+", "(") + while (!bracketsClosed(fixBrackets)) { + fixBrackets += ')' + } + return fixBrackets.replace("()", "") // fix empty brackets +} + +/** + * Returns a normalized expression by: + * - removing grouping separators for thousands + * - replacing local decimal separator with '.' + * 1,032.55 => 1032.55 + */ +fun normalize(expression: String): String = expression + .replace(localGroupingSeparator().toString(), "") + .replace(localDecimalSeparator().toString(), ".") diff --git a/math/src/main/java/com/ivy/math/ExpressionParser.kt b/math/src/main/java/com/ivy/math/ExpressionParser.kt new file mode 100644 index 0000000000..8c53d8df0f --- /dev/null +++ b/math/src/main/java/com/ivy/math/ExpressionParser.kt @@ -0,0 +1,110 @@ +package com.ivy.math + +import arrow.core.NonEmptyList +import arrow.core.nonEmptyListOf +import com.ivy.parser.* +import com.ivy.parser.common.number + +sealed interface TreeNode { + fun print(): String + fun eval(): Double +} + +class Add(private val things: NonEmptyList) : TreeNode { + override fun print(): String = things.map { "(${it.print()})" } + .joinToString(separator = "+") + + override fun eval(): Double = things.map { it.eval() }.sum() +} + +class Multiply(private val left: TreeNode, private val right: TreeNode) : TreeNode { + override fun print(): String = "(${left.print()}*${right.print()}.)" + + override fun eval(): Double = left.eval() * right.eval() +} + +class Divide(private val left: TreeNode, private val right: TreeNode) : TreeNode { + override fun print(): String = "(${left.print()}/${right.print()}.)" + + override fun eval(): Double = left.eval() / right.eval() +} + +class Percent(private val expr: TreeNode) : TreeNode { + override fun print(): String = "(${expr.print()})%" + + override fun eval(): Double = expr.eval() / 100.0 +} + +class Negate(private val node: TreeNode) : TreeNode { + override fun print(): String = "(-${node.print()})" + + override fun eval(): Double = -(node.eval()) +} + +class Number(private val decimal: Double) : TreeNode { + override fun print(): String = decimal.toString() + + override fun eval(): Double = decimal +} + + +/** + * Evaluates an arbitrary mathematical expression to double. + */ +fun expressionParser(): Parser = expr() + +private fun expr(): Parser = term().apply { x -> + oneOrMany( + (char('+') or char('-')).apply { sign -> + term().apply { y -> + pure( + when (sign) { + '+' -> y + '-' -> Negate(y) + else -> error("Impossible") + } + ) + } + } + ).apply { ys -> + pure(Add(nonEmptyListOf(x, *ys.toTypedArray()))) + } +} or term() + +private fun term(): Parser = factor().apply { x -> + char('*').apply { + term().apply { y -> + pure(Multiply(x, y)) + } + } +} or factor().apply { x -> + char('/').apply { + term().apply { y -> + pure(Divide(x, y)) + } + } +} or factor() + +private fun factor(): Parser = number().apply { x -> + char('%').apply { + pure(Percent(Number(x))) + } +} or number().apply { num -> + pure(Number(num)) +} or char('(').apply { + expr().apply { x -> + string(")%").apply { + pure(Percent(x)) + } + } +} or char('(').apply { + expr().apply { x -> + char(')').apply { + pure(x) + } + } +} or char('-').apply { + factor().apply { x -> + pure(Negate(x)) + } +} \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/FormatNumber.kt b/math/src/main/java/com/ivy/math/FormatNumber.kt new file mode 100644 index 0000000000..c5bf86717f --- /dev/null +++ b/math/src/main/java/com/ivy/math/FormatNumber.kt @@ -0,0 +1,10 @@ +package com.ivy.math + +import java.text.DecimalFormat + +/** + * Formats number like a calculator would do. + * Precision is set to 6 decimals. + */ +fun formatNumber(number: Double): String = + DecimalFormat("###,###,##0.${"#".repeat(6)}").format(number) \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/LocalSeparators.kt b/math/src/main/java/com/ivy/math/LocalSeparators.kt new file mode 100644 index 0000000000..8286fb360e --- /dev/null +++ b/math/src/main/java/com/ivy/math/LocalSeparators.kt @@ -0,0 +1,9 @@ +package com.ivy.math + +import java.text.DecimalFormatSymbols + +fun localDecimalSeparator(): Char = + DecimalFormatSymbols.getInstance().decimalSeparator + +fun localGroupingSeparator(): Char = + DecimalFormatSymbols.getInstance().groupingSeparator \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/calculator/AppendCalculatorOperator.kt b/math/src/main/java/com/ivy/math/calculator/AppendCalculatorOperator.kt new file mode 100644 index 0000000000..ef8e223a38 --- /dev/null +++ b/math/src/main/java/com/ivy/math/calculator/AppendCalculatorOperator.kt @@ -0,0 +1,83 @@ +package com.ivy.math.calculator + +import com.ivy.math.expressionParser +import com.ivy.math.normalize +import com.ivy.parser.common.number + +/** + * Appends calculator option to an expression by following expression syntax. + * If the calculator option isn't valid it won't be added. + * @return a new expression with the selected calculator option applied. + */ +fun appendTo(expression: String, operator: CalculatorOperator): String = when (operator) { + CalculatorOperator.Plus -> expression.appendPlusOrMinus('+') + CalculatorOperator.Minus -> expression.appendPlusOrMinus('-') + CalculatorOperator.Multiply -> expression.appendBinaryOperator('*') + CalculatorOperator.Divide -> expression.appendBinaryOperator('/') + CalculatorOperator.Brackets -> expression.brackets() + CalculatorOperator.Percent -> expression.percent() +} + +private fun String.appendPlusOrMinus(operator: Char): String = when (this.lastOrNull()) { + '-', '+' -> this.dropLast(1).plus(operator) + else -> this.plus(operator) +} + +private fun String.appendBinaryOperator(operator: Char): String { + when (this.lastOrNull()) { + // binary operators can be applied to '%' and ')' + '%', ')' -> return this.plus(operator) + } + // binary operators require a number on the left + return if (endWithDecimal(this)) this.plus(operator) else this +} + +fun bracketsClosed(expression: String): Boolean = + expression.count { it == '(' } == expression.count { it == ')' } + + +private fun String.brackets(): String { + fun determineBracket(expression: String): String { + if (expression.isEmpty()) return "(" + val closed = bracketsClosed(expression) + return when (expression.lastOrNull()) { + '+', '-', '(', '/', '*' -> "(" + ')' -> if (closed) "*(" else ")" + else -> { + if (!closed) return ")" + val parsed = expressionParser().invoke(expression) + if (parsed.isNotEmpty()) return "*(" + ")" + } + } + } + + return this + determineBracket(this) +} + +private fun String.percent(): String { + fun allowPercent(expression: String): Boolean = when (expression.lastOrNull()) { + ')' -> true + null, '+', '-', '*', '/', '%' -> false + else -> endWithDecimal(this) + } + + return if (allowPercent(this)) this.plus('%') else this +} + +private fun endWithDecimal(expression: String): Boolean { + /** + * Extracts the last number from an expression. + * 10+15.5 => 15.5 + */ + fun lastNumber(expression: String): String? { + val lastChar = expression.lastOrNull() ?: return null + return lastChar + (lastNumber(expression.dropLast(1)) ?: "") + } + + val normalizedExpression = normalize(expression) + val lastNumber = lastNumber(normalizedExpression) + ?: return false // binary expressions require a number on the left! + val decimalResult = number().invoke(lastNumber) + return decimalResult.isNotEmpty() // parsed successfully a decimal +} \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/calculator/AppendDecimalSeparator.kt b/math/src/main/java/com/ivy/math/calculator/AppendDecimalSeparator.kt new file mode 100644 index 0000000000..19e5c09484 --- /dev/null +++ b/math/src/main/java/com/ivy/math/calculator/AppendDecimalSeparator.kt @@ -0,0 +1,23 @@ +package com.ivy.math.calculator + +fun appendDecimalSeparator( + expression: String, decimalSeparator: Char +): String { + fun allowDecimalSeparator(expression: String): Boolean = + when (expression.lastOrNull()) { + ')', decimalSeparator, '%' -> false + else -> true + } + + fun appendDecimalSeparator(expression: String): String { + val lastChar = expression.lastOrNull() + return when { + lastChar == null -> expression.plus("0.") + !lastChar.isDigit() -> expression.plus("0.") + else -> expression.plus('.') + } + } + + return if (allowDecimalSeparator(expression)) + appendDecimalSeparator(expression) else expression +} \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/calculator/AppendNumberDigit.kt b/math/src/main/java/com/ivy/math/calculator/AppendNumberDigit.kt new file mode 100644 index 0000000000..6049191280 --- /dev/null +++ b/math/src/main/java/com/ivy/math/calculator/AppendNumberDigit.kt @@ -0,0 +1,9 @@ +package com.ivy.math.calculator + +fun appendTo(expression: String, digit: Int): String { + val thingToAppend = when (expression.lastOrNull()) { + ')', '%' -> "*$digit" + else -> "$digit" + } + return expression.plus(thingToAppend) +} \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/calculator/CalculatorOperator.kt b/math/src/main/java/com/ivy/math/calculator/CalculatorOperator.kt new file mode 100644 index 0000000000..f5f1f4f0b7 --- /dev/null +++ b/math/src/main/java/com/ivy/math/calculator/CalculatorOperator.kt @@ -0,0 +1,5 @@ +package com.ivy.math.calculator + +enum class CalculatorOperator { + Plus, Minus, Multiply, Divide, Brackets, Percent +} \ No newline at end of file diff --git a/math/src/main/java/com/ivy/math/calculator/ExpressionUtil.kt b/math/src/main/java/com/ivy/math/calculator/ExpressionUtil.kt new file mode 100644 index 0000000000..abdf6a779d --- /dev/null +++ b/math/src/main/java/com/ivy/math/calculator/ExpressionUtil.kt @@ -0,0 +1,42 @@ +package com.ivy.math.calculator + +import com.ivy.math.localDecimalSeparator +import com.ivy.math.normalize +import java.text.DecimalFormat + +/** + * @return whether the calculation result is worth to be displayed. + */ +fun hasObviousResult(expression: String, value: Double?): Boolean = + when (expression.lastOrNull()) { + '+', '-', '*', '/' -> expression.dropLast(1).none { + // It's obvious if it has any preceding calculations + when (it) { + '+', '-', '*', '/' -> true + else -> false + } + } + else -> normalize(expression).toDoubleOrNull() == value + } + +fun beautify(expression: String): String? { + fun formatInt(number: String): String = + number.toIntOrNull()?.let { DecimalFormat("###,###,###").format(it) } ?: number + + fun format(number: String): String = if (number.contains(localDecimalSeparator())) { + // format decimal + val (int, decimal) = number.split(localDecimalSeparator()) + "${formatInt(int)}.$decimal" + } else { + formatInt(number) + } + + if (expression.isEmpty()) return null + var beautified = expression + expression.split("+", "-", "*", "/", "(", ")", "%") + .forEach { numberStr -> + val formattedNum = format(numberStr) + beautified = beautified.replace(numberStr, formattedNum) + } + return beautified +} \ No newline at end of file diff --git a/math/src/test/java/com/ivy/math/EvaluateExpressionTest.kt b/math/src/test/java/com/ivy/math/EvaluateExpressionTest.kt new file mode 100644 index 0000000000..d30458342f --- /dev/null +++ b/math/src/test/java/com/ivy/math/EvaluateExpressionTest.kt @@ -0,0 +1,32 @@ +package com.ivy.math + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class EvaluateExpressionTest : FreeSpec({ + "evaluates an expression" - { + withData( + nameFn = { (expression, value) -> + "\"$expression\" to $value" + }, + // Expression (evaluate as) Value + row("1,024+0.99-", 1_024.99), + row("3+", 3.0), + row("2+2", 4.0), + row("-9-", -9.0), + row("7*", 7.0), + row("4/", 4.0), + row("(12.0", 12.0), + row("8/()", 8.0), + row("45+(", 45.0), + row("+20-45+10-45%*10", -19.5), + row("7*(+5-3+1-2)", 7.0), + ) { (expression, value) -> + val res = evaluate(expression) + + res shouldBe value + } + } +}) \ No newline at end of file diff --git a/math/src/test/java/com/ivy/math/ExpressionParserTest.kt b/math/src/test/java/com/ivy/math/ExpressionParserTest.kt new file mode 100644 index 0000000000..abe7a875b0 --- /dev/null +++ b/math/src/test/java/com/ivy/math/ExpressionParserTest.kt @@ -0,0 +1,55 @@ +package com.ivy.math + +import com.ivy.parser.ParseResult +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class ExpressionParserTest : FreeSpec({ + "evaluates an expression" - { + withData( + nameFn = { (expression, value) -> + "\"$expression\" = $value" + }, + // Expression (=) Value + row("3.14", 3.14), + row("2+2", 4.0), + row("5-5", 0.0), + row("6*9", 54.0), + row("17/4", 4.25), + row("2+3*2", 8.0), + row("3*3.0*3", 27.0), + row("(-7.5)+3.25-1*1", -5.25), + row("(((24000-1400)*10%)-7200.50*6)/4", -10_235.75), + row("-(5)", -5.0), + row("-(3*3)", -9.0), + row("(0.5)", 0.5), + row("((20/5)*4)", 16.0), + row("(-(-1))", 1.0), + row("(2+2)%", 0.04), + row("10%*200", 20.0), + row("25%+0.75", 1.0), + row("1-1+1-1", 0.0), + row("1-1", 0.0), + row("1-1-1", -1.0), + row("1-1-1-1", -2.0), + row("-8-4+2", -10.0), + row("1000000-(12*12534-12*12534*10%)*80%", 891706.24), + ) { (expression, expectedValue) -> + val parser = expressionParser() + + val parseResults = parser(expression) + val res = parseResults.map { + val expressionTree = it.value + val value = expressionTree.eval() + println( + "\"$expression\" becomes \"${expressionTree.print()}\" and evaluates as $value" + ) + ParseResult(value, it.leftover) + } + + res shouldBe listOf(ParseResult(expectedValue, "")) + } + } +}) \ No newline at end of file diff --git a/math/src/test/java/com/ivy/math/FormatNumberTest.kt b/math/src/test/java/com/ivy/math/FormatNumberTest.kt new file mode 100644 index 0000000000..7d0b7be887 --- /dev/null +++ b/math/src/test/java/com/ivy/math/FormatNumberTest.kt @@ -0,0 +1,28 @@ +package com.ivy.math + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class FormatNumberTest : FreeSpec({ + "formats number" - { + withData( + nameFn = { (number, expected) -> "$number as \"$expected\"" }, + // Number (as) String + row(5.0, "5"), + row(3.141_323, "3.141323"), + row(0.005, "0.005"), + row(1_024.0, "1,024"), + row(10_030.25, "10,030.25"), + row(1.000_004_000_00, "1.000004"), + row(-1.0, "-1"), + row(.5, "0.5"), + row(0.25, "0.25"), + ) { (number, expected) -> + val res = formatNumber(number) + + res shouldBe expected + } + } +}) \ No newline at end of file diff --git a/math/src/test/java/com/ivy/math/calculator/AppendCalculatorOperatorTest.kt b/math/src/test/java/com/ivy/math/calculator/AppendCalculatorOperatorTest.kt new file mode 100644 index 0000000000..c3016de76b --- /dev/null +++ b/math/src/test/java/com/ivy/math/calculator/AppendCalculatorOperatorTest.kt @@ -0,0 +1,157 @@ +package com.ivy.math.calculator + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.Row2 +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class AppendCalculatorOperatorTest : FreeSpec({ + fun nameFn(): (Row2) -> String = { (expression, expected) -> + "\"$expression\" becomes \"$expected\"" + } + + "appending '+' to an expression" - { + withData( + nameFn = nameFn(), + // Expression before (becomes) Expression (after) + row("", "+"), + row("3", "3+"), + row("5+", "5+"), // doesn't apply double plus + row("(", "(+"), + row("2/", "2/+"), + row("*", "*+"), + row("%", "%+"), + row("-", "+"), + row("0.23-", "0.23+"), + row("(3*3)", "(3*3)+"), + // swapping operators + row("2-", "2+"), + ) { (expression, expected) -> + val res = appendTo(expression, CalculatorOperator.Plus) + + res shouldBe expected + } + } + + "appending '-' to an expression" - { + withData( + nameFn = nameFn(), + // Expression before (becomes) Expression (after) + row("", "-"), + row("3", "3-"), + row("5-", "5-"), // doesn't apply double plus + row("(", "(-"), + row("2/", "2/-"), + row("*", "*-"), + row("%", "%-"), + row("+", "-"), + row("0.23-", "0.23-"), + row("(5+5)", "(5+5)-"), + // swapping operators + row("7+", "7-") + ) { (expression, expected) -> + val res = appendTo(expression, CalculatorOperator.Minus) + + res shouldBe expected + } + } + + "appending '*' to an expression" - { + withData( + nameFn = nameFn(), + // Expression before (becomes) Expression (after) + row("1", "1*"), + row("232.99", "232.99*"), + row(".5", ".5*"), + row("1.", "1.*"), + row("", ""), + row("+", "+"), + row("-", "-"), + row("/", "/"), + row("*", "*"), + row("10%", "10%*"), + row("(", "("), + row(")", ")*"), + row("-(9", "-(9*"), + ) { (expression, expected) -> + val res = appendTo(expression, CalculatorOperator.Multiply) + + res shouldBe expected + } + } + + "appending '/' to an expression" - { + withData( + nameFn = nameFn(), + // Expression before (becomes) Expression (after) + row("1", "1/"), + row("3.14", "3.14/"), + row(".5", ".5/"), + row("1.", "1./"), + row("", ""), + row("+", "+"), + row("-", "-"), + row("/", "/"), + row("*", "*"), + row("10%", "10%/"), + row("(", "("), + row(")", ")/"), + row("-(9", "-(9/"), + ) { (expression, expected) -> + val res = appendTo(expression, CalculatorOperator.Divide) + + res shouldBe expected + } + } + + "appending brackets '()' to an expression" - { + withData( + nameFn = nameFn(), + // Expression before (becomes) Expression (after) + row("", "("), + row("(", "(("), + row("-((", "-((("), + row("(3", "(3)"), + row("-(-25.0+10", "-(-25.0+10)"), + row("1000/(300*3", "1000/(300*3)"), + row("(3+", "(3+("), + row("+(.25", "+(.25)"), + row("((10+10)*33", "((10+10)*33)"), + row("3", "3*("), + row("2/", "2/("), + row("(2+2)", "(2+2)*("), + row("((50/5", "((50/5)"), + row("((50/5)", "((50/5))"), + row("((-", "((-("), + row("10+10", "10+10*("), + row("0.5", "0.5*("), + row("5*", "5*("), + ) { (expression, expected) -> + val res = appendTo(expression, CalculatorOperator.Brackets) + + res shouldBe expected + } + } + + "appending % to an expression" - { + withData( + nameFn = nameFn(), + // Expression before (becomes) Expression (after) + row("", ""), + row("10%", "10%"), + row("10", "10%"), + row("(5+5)", "(5+5)%"), + row(".3", ".3%"), + row(".3", ".3%"), + row("5+", "5+"), + row("-", "-"), + row("*", "*"), + row("/", "/"), + ) { (expression, expected) -> + val res = appendTo(expression, CalculatorOperator.Percent) + + res shouldBe expected + } + } +}) \ No newline at end of file diff --git a/math/src/test/java/com/ivy/math/calculator/AppendDecimalSeparatorTest.kt b/math/src/test/java/com/ivy/math/calculator/AppendDecimalSeparatorTest.kt new file mode 100644 index 0000000000..4bcd91f654 --- /dev/null +++ b/math/src/test/java/com/ivy/math/calculator/AppendDecimalSeparatorTest.kt @@ -0,0 +1,28 @@ +package com.ivy.math.calculator + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class AppendDecimalSeparatorTest : FreeSpec({ + "appending '.' decimal separator" - { + withData( + nameFn = { (expression, expected) -> + "\"$expression\" becomes \"$expected\"" + }, + // Expression (becomes) Expression after + row("", "0."), + row(".", "."), + row("(10+5)", "(10+5)"), + row("7", "7."), + row("2%", "2%"), + ) { (expression, expected) -> + val res = appendDecimalSeparator( + expression = expression, decimalSeparator = '.' + ) + + res shouldBe expected + } + } +}) \ No newline at end of file diff --git a/math/src/test/java/com/ivy/math/calculator/AppendNumberDigit.kt b/math/src/test/java/com/ivy/math/calculator/AppendNumberDigit.kt new file mode 100644 index 0000000000..6cf1668613 --- /dev/null +++ b/math/src/test/java/com/ivy/math/calculator/AppendNumberDigit.kt @@ -0,0 +1,29 @@ +package com.ivy.math.calculator + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.next + +class AppendNumberDigit : FreeSpec({ + "appending digit to an expression" - { + val digit = Arb.int(0..9).next() + withData( + nameFn = { (expression, expected) -> + "\"$expression\" becomes \"$expected\"" + }, + // Expression (becomes) Expression after + row("", "$digit"), + row("1", "1$digit"), + row("10%", "10%*$digit"), + row("(5+5)", "(5+5)*$digit"), + ) { (expression, expected) -> + val res = appendTo(expression, digit) + + res shouldBe expected + } + } +}) \ No newline at end of file diff --git a/math/src/test/java/com/ivy/math/calculator/ExpressionUtilTest.kt b/math/src/test/java/com/ivy/math/calculator/ExpressionUtilTest.kt new file mode 100644 index 0000000000..04ef3fd2ff --- /dev/null +++ b/math/src/test/java/com/ivy/math/calculator/ExpressionUtilTest.kt @@ -0,0 +1,51 @@ +package com.ivy.math.calculator + +import com.ivy.math.evaluate +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class ExpressionUtilTest : FreeSpec({ + "classifies an expression" - { + withData( + nameFn = { (expression, obvious) -> + "\"$expression\" as ${if (obvious) "obvious" else "NOT obvious"}" + }, + // Expression (as) Obvious? + row("32", true), + row("513.00+", true), + row("513.00+1", false), + row("513.00+(", false), + row("10*", true), + row("10/", true), + row("0.24-", true), + row("8.00+9*", false), + ) { (expression, obvious) -> + val value = evaluate(expression) + + val res = hasObviousResult(expression, value) + + res shouldBe obvious + } + } + + "beautifies an expression" - { + withData( + nameFn = { (expression, beautified) -> + "\"$expression\" beautified to \"$beautified\"" + }, + // Expression (as) Beautified expression + row("", null), + row("3.14", "3.14"), + row("5+5", "5+5"), + row("1024", "1,024"), + row("1000000", "1,000,000"), + row("10123.45678", "10,123.45678"), + ) { (expression, beautified) -> + val res = beautify(expression) + + res shouldBe beautified + } + } +}) \ No newline at end of file diff --git a/navigation/src/main/java/com/ivy/navigation/NavigationRoot.kt b/navigation/src/main/java/com/ivy/navigation/NavigationRoot.kt index e24fe2a57c..d43da93e73 100644 --- a/navigation/src/main/java/com/ivy/navigation/NavigationRoot.kt +++ b/navigation/src/main/java/com/ivy/navigation/NavigationRoot.kt @@ -6,7 +6,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.ivy.navigation.destinations.Destination +import com.ivy.navigation.destinations.main.Categories import com.ivy.navigation.destinations.main.Main +import com.ivy.navigation.destinations.settings.Settings import com.ivy.navigation.graph.* import kotlinx.coroutines.flow.collectLatest @@ -15,6 +17,8 @@ fun NavigationRoot( navigator: Navigator, onboardingScreens: OnboardingScreens, main: @Composable (Main.Tab?) -> Unit, + categories: @Composable () -> Unit, + settings: @Composable () -> Unit, transactionScreens: TransactionScreens, debugScreens: DebugScreens ) { @@ -38,6 +42,12 @@ fun NavigationRoot( composable(Main.route) { main(Main.parse(it)) } + composable(Categories.route) { + categories() + } + composable(Settings.route) { + settings() + } transactionScreens(transactionScreens) debug(debugScreens) } diff --git a/navigation/src/main/java/com/ivy/navigation/destinations/Destination.kt b/navigation/src/main/java/com/ivy/navigation/destinations/Destination.kt index 24fc99d4f8..d0868c71be 100644 --- a/navigation/src/main/java/com/ivy/navigation/destinations/Destination.kt +++ b/navigation/src/main/java/com/ivy/navigation/destinations/Destination.kt @@ -2,8 +2,10 @@ package com.ivy.navigation.destinations import com.ivy.navigation.destinations.debug.DebugGraph import com.ivy.navigation.destinations.imports.ImportGraph +import com.ivy.navigation.destinations.main.Categories import com.ivy.navigation.destinations.main.Main import com.ivy.navigation.destinations.onboarding.OnboardingGraph +import com.ivy.navigation.destinations.settings.Settings import com.ivy.navigation.destinations.transaction.* object Destination { @@ -21,5 +23,8 @@ object Destination { val categoryTransactions = CategoryTransactions // endregion + val categories = Categories + val settings = Settings + val debug = DebugGraph } \ No newline at end of file diff --git a/navigation/src/main/java/com/ivy/navigation/destinations/main/Categories.kt b/navigation/src/main/java/com/ivy/navigation/destinations/main/Categories.kt new file mode 100644 index 0000000000..a08c8e8298 --- /dev/null +++ b/navigation/src/main/java/com/ivy/navigation/destinations/main/Categories.kt @@ -0,0 +1,15 @@ +package com.ivy.navigation.destinations.main + +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavBackStackEntry +import com.ivy.navigation.DestinationRoute +import com.ivy.navigation.Screen + +object Categories : Screen { + override val route: String = "categories" + override val arguments: List = emptyList() + + override fun parse(entry: NavBackStackEntry): Unit = Unit + + override fun destination(arg: Unit): DestinationRoute = route +} \ No newline at end of file diff --git a/navigation/src/main/java/com/ivy/navigation/destinations/main/Main.kt b/navigation/src/main/java/com/ivy/navigation/destinations/main/Main.kt index 5d4b231eb4..cc54aaf214 100644 --- a/navigation/src/main/java/com/ivy/navigation/destinations/main/Main.kt +++ b/navigation/src/main/java/com/ivy/navigation/destinations/main/Main.kt @@ -18,7 +18,7 @@ object Main : Screen { private const val ARG_TAB = "tab" - override val route: String = "main?tab=${ARG_TAB}" + override val route: String = "main?tab={$ARG_TAB}" override val arguments: List = listOf( navArgument(ARG_TAB) { type = NavType.StringType diff --git a/navigation/src/main/java/com/ivy/navigation/destinations/settings/Settings.kt b/navigation/src/main/java/com/ivy/navigation/destinations/settings/Settings.kt new file mode 100644 index 0000000000..bd95a9fa9c --- /dev/null +++ b/navigation/src/main/java/com/ivy/navigation/destinations/settings/Settings.kt @@ -0,0 +1,15 @@ +package com.ivy.navigation.destinations.settings + +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavBackStackEntry +import com.ivy.navigation.DestinationRoute +import com.ivy.navigation.Screen + +object Settings : Screen { + override val route = "/settings" + override val arguments: List = emptyList() + + override fun parse(entry: NavBackStackEntry) {} + + override fun destination(arg: Unit): DestinationRoute = route +} \ No newline at end of file diff --git a/navigation/src/main/java/com/ivy/navigation/destinations/transaction/NewTransaction.kt b/navigation/src/main/java/com/ivy/navigation/destinations/transaction/NewTransaction.kt index 8eeb4a6e92..b001f7997c 100644 --- a/navigation/src/main/java/com/ivy/navigation/destinations/transaction/NewTransaction.kt +++ b/navigation/src/main/java/com/ivy/navigation/destinations/transaction/NewTransaction.kt @@ -13,20 +13,20 @@ import com.ivy.navigation.util.optionalStringArg object NewTransaction : Screen { data class Arg( val trnType: TransactionType, - val categoryId: String?, - val accountId: String?, + val categoryId: String? = null, + val accountId: String? = null, ) private const val ARG_TRN_TYPE = "trnType" private const val ARG_CATEGORY_ID = "catId" private const val ARG_ACCOUNT_ID = "accId" - override val route: String = "new/transaction?trnType={$ARG_TRN_TYPE}" + - "&catId={$ARG_CATEGORY_ID}&accId={$ARG_ACCOUNT_ID}" + override val route: String = "new/transaction?$ARG_TRN_TYPE={$ARG_TRN_TYPE}" + + "&$ARG_CATEGORY_ID={$ARG_CATEGORY_ID}&$ARG_ACCOUNT_ID={$ARG_ACCOUNT_ID}" override val arguments = listOf( navArgument(ARG_TRN_TYPE) { - type = NavType.IntType + type = NavType.StringType nullable = false }, navArgument(ARG_CATEGORY_ID) { @@ -52,7 +52,7 @@ object NewTransaction : Screen { override fun parse(entry: NavBackStackEntry): Arg = Arg( trnType = entry.arg(ARG_TRN_TYPE, int()) { - TransactionType.fromCode(it) ?: TransactionType.Expense + TransactionType.fromCode((it)) ?: TransactionType.Expense }, categoryId = entry.optionalStringArg(ARG_CATEGORY_ID), accountId = entry.optionalStringArg(ARG_ACCOUNT_ID), diff --git a/navigation/src/main/java/com/ivy/navigation/util/NavBackStackEntryExt.kt b/navigation/src/main/java/com/ivy/navigation/util/NavBackStackEntryExt.kt index ac1247e693..946f3d1888 100644 --- a/navigation/src/main/java/com/ivy/navigation/util/NavBackStackEntryExt.kt +++ b/navigation/src/main/java/com/ivy/navigation/util/NavBackStackEntryExt.kt @@ -20,5 +20,5 @@ fun NavBackStackEntry.optionalArg( ): Arg? = type(key)?.let(transform) fun string(): NavBackStackEntry.(String) -> String? = { key -> arguments?.getString(key) } -fun int(): NavBackStackEntry.(String) -> Int? = { key -> arguments?.getInt(key) } +fun int(): NavBackStackEntry.(String) -> Int? = { key -> arguments?.getString(key)?.toIntOrNull() } fun bool(): NavBackStackEntry.(String) -> Boolean? = { key -> arguments?.getBoolean(key) } \ No newline at end of file diff --git a/network/build.gradle.kts b/network/build.gradle.kts index 4294356786..0144f8c97f 100644 --- a/network/build.gradle.kts +++ b/network/build.gradle.kts @@ -11,6 +11,5 @@ plugins { dependencies { Hilt() implementation(project(":common:main")) - api(project(":temp-network")) Ktor(api = true) } \ No newline at end of file diff --git a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingAccounts.kt b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingAccounts.kt index 74152e479e..9a4ab229d5 100644 --- a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingAccounts.kt +++ b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingAccounts.kt @@ -17,7 +17,7 @@ //import androidx.compose.ui.tooling.preview.Preview //import androidx.compose.ui.unit.dp //import com.ivy.base.AccountBalance -//import com.ivy.base.R +//import com.ivy.resources.R //import com.ivy.data.AccountOld //import com.ivy.design.l0_system.UI //import com.ivy.design.l0_system.style diff --git a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingCategories.kt b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingCategories.kt index 1633624bb0..6e2a1e970c 100644 --- a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingCategories.kt +++ b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingCategories.kt @@ -19,7 +19,7 @@ //import androidx.compose.ui.text.font.FontWeight //import androidx.compose.ui.tooling.preview.Preview //import androidx.compose.ui.unit.dp -//import com.ivy.base.R +//import com.ivy.resources.R //import com.ivy.data.CategoryOld //import com.ivy.design.l0_system.UI //import com.ivy.design.l0_system.style diff --git a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSetCurrency.kt b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSetCurrency.kt index c77071f1fe..9fe560faa2 100644 --- a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSetCurrency.kt +++ b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSetCurrency.kt @@ -9,7 +9,7 @@ //import androidx.compose.ui.text.font.FontWeight //import androidx.compose.ui.tooling.preview.Preview //import androidx.compose.ui.unit.dp -//import com.ivy.base.R +//import com.ivy.resources.R //import com.ivy.data.IvyCurrency //import com.ivy.design.l0_system.UI //import com.ivy.design.l0_system.style diff --git a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSplashLogin.kt b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSplashLogin.kt index 9defa8b053..024f77b31e 100644 --- a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSplashLogin.kt +++ b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingSplashLogin.kt @@ -31,7 +31,7 @@ //import androidx.compose.ui.unit.Dp //import androidx.compose.ui.unit.dp //import com.ivy.base.Constants -//import com.ivy.base.R +//import com.ivy.resources.R //import com.ivy.design.l0_system.UI //import com.ivy.design.l0_system.style //import com.ivy.design.util.IvyPreview diff --git a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingType.kt b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingType.kt index 3792c1f497..a81ce03a39 100644 --- a/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingType.kt +++ b/onboarding/src/main/java/com/ivy/onboarding/old/steps/OnboardingType.kt @@ -11,7 +11,7 @@ //import androidx.compose.ui.text.font.FontWeight //import androidx.compose.ui.tooling.preview.Preview //import androidx.compose.ui.unit.dp -//import com.ivy.base.R +//import com.ivy.resources.R //import com.ivy.design.l0_system.UI //import com.ivy.design.l0_system.style //import com.ivy.design.util.IvyPreview diff --git a/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugScreen.kt b/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugScreen.kt index a2ce1eeca3..eabd1a5de1 100644 --- a/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugScreen.kt +++ b/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugScreen.kt @@ -9,9 +9,9 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.ivy.data.CurrencyCode import com.ivy.design.l1_buildingBlocks.* -import com.ivy.design.l3_ivyComponents.button.ButtonFeeling +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility import com.ivy.design.l3_ivyComponents.button.ButtonSize -import com.ivy.design.l3_ivyComponents.button.ButtonVisibility import com.ivy.design.l3_ivyComponents.button.IvyButton @Composable @@ -57,8 +57,8 @@ fun BoxScope.OnboardingDebug() { IvyButton( modifier = Modifier.padding(horizontal = 16.dp), size = ButtonSize.Big, - visibility = ButtonVisibility.Focused, - feeling = ButtonFeeling.Positive, + visibility = Visibility.Focused, + feeling = Feeling.Positive, text = "Finish onboarding", icon = null ) { @@ -77,8 +77,8 @@ private fun CurrencyButton( IvyButton( size = ButtonSize.Small, visibility = if (currency == baseCurrency) - ButtonVisibility.High else ButtonVisibility.Medium, - feeling = ButtonFeeling.Positive, + Visibility.High else Visibility.Medium, + feeling = Feeling.Positive, text = currency, icon = null ) { diff --git a/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugViewModel.kt b/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugViewModel.kt index adf124e8df..7dc19f2295 100644 --- a/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugViewModel.kt +++ b/onboarding/src/main/java/com/ivy/onboarding/screen/debug/OnboardingDebugViewModel.kt @@ -1,6 +1,6 @@ package com.ivy.onboarding.screen.debug -import com.ivy.core.domain.FlowViewModel +import com.ivy.core.domain.SimpleFlowViewModel import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow import com.ivy.core.domain.action.settings.basecurrency.WriteBaseCurrencyAct import com.ivy.navigation.Navigator @@ -15,16 +15,12 @@ import javax.inject.Inject class OnboardingDebugViewModel @Inject constructor( private val writeBaseCurrencyAct: WriteBaseCurrencyAct, private val writeOnboardingFinishedAct: WriteOnboardingFinishedAct, - private val baseCurrencyFlow: BaseCurrencyFlow, + baseCurrencyFlow: BaseCurrencyFlow, private val navigator: Navigator, -) : FlowViewModel() { - override fun initialState() = OnboardingDebugState(baseCurrency = "") +) : SimpleFlowViewModel() { + override val initialUi = OnboardingDebugState(baseCurrency = "") - override fun initialUiState() = initialState() - - override suspend fun mapToUiState(state: OnboardingDebugState): OnboardingDebugState = state - - override fun stateFlow(): Flow = baseCurrencyFlow().map { + override val uiFlow: Flow = baseCurrencyFlow().map { OnboardingDebugState(baseCurrency = it) } diff --git a/main/.gitignore b/parser/.gitignore similarity index 100% rename from main/.gitignore rename to parser/.gitignore diff --git a/parser/README.md b/parser/README.md new file mode 100644 index 0000000000..9d83afda88 --- /dev/null +++ b/parser/README.md @@ -0,0 +1,10 @@ +# Parser + +A functional recursive descent parser used for implementing +Ivy Wallet's calculator expressions and formulas. + +It's motivated by and +implements [FUNCTIONAL PEARLS Monadic Parsing in Haskell](https://www.cs.nott.ac.uk/~pszgmh/pearl.pdf) +in Kotlin. + +The purpose of this module is to allow its users to parse any arbitrary text in a concise manner. \ No newline at end of file diff --git a/sync/base/build.gradle.kts b/parser/build.gradle.kts similarity index 80% rename from sync/base/build.gradle.kts rename to parser/build.gradle.kts index 94bad597c4..a0e634a7e4 100644 --- a/sync/base/build.gradle.kts +++ b/parser/build.gradle.kts @@ -1,4 +1,5 @@ import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing apply() @@ -10,4 +11,5 @@ plugins { dependencies { Hilt() implementation(project(":common:main")) + Testing() } \ No newline at end of file diff --git a/balance-prediction/src/main/AndroidManifest.xml b/parser/src/main/AndroidManifest.xml similarity index 51% rename from balance-prediction/src/main/AndroidManifest.xml rename to parser/src/main/AndroidManifest.xml index 78c67b0668..45d62f8c5d 100644 --- a/balance-prediction/src/main/AndroidManifest.xml +++ b/parser/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/parser/src/main/java/com/ivy/parser/LexicalCombinators.kt b/parser/src/main/java/com/ivy/parser/LexicalCombinators.kt new file mode 100644 index 0000000000..756f372b7b --- /dev/null +++ b/parser/src/main/java/com/ivy/parser/LexicalCombinators.kt @@ -0,0 +1,29 @@ +package com.ivy.parser + +/** + * Parses whitespace ' ' (space), '\t' tab or '\n'. + * This operation never fails. For empty text, simple returns empty list of chars. + */ +fun whitespace(): Parser> = zeroOrMany(sat { it.isWhitespace() }) + +/** + * Parses a thing and then removes the whitespace after it. + * + * **Example** + * ``` + * val parser = token(string("okay")) + * parser("okay Google") + * // ParserResult(value="okay", leftover = "Google") + * ``` + */ +fun token(parser: Parser): Parser = parser.apply { t -> + whitespace().apply { + pure(t) + } +} + +/** + * Parses a string and then remove the whitespace after it. + * See [token]. + */ +fun symbolicToken(str: String): Parser = token(string(str)) \ No newline at end of file diff --git a/parser/src/main/java/com/ivy/parser/Parser.kt b/parser/src/main/java/com/ivy/parser/Parser.kt new file mode 100644 index 0000000000..deb56495ab --- /dev/null +++ b/parser/src/main/java/com/ivy/parser/Parser.kt @@ -0,0 +1,146 @@ +package com.ivy.parser + +/** + * Motivated by FUNCTIONAL PEARL + * Monadic parsing in Haskell + * by Graham Hutton & Erik Meijer + */ +// Paper: https://www.cs.nott.ac.uk/~pszgmh/pearl.pdf + +/** + * @param value the parsed value + * @param leftover the text left to parse + */ +data class ParseResult( + val value: T, + val leftover: String, +) + +/** + * Parser monad which accepts text (String) + * and returns a list of parse interpretations or [] on failure. + */ +typealias Parser = (String) -> List> + +// region Result builders +/** + * Use for successfully parsing a value. + * Wraps a value in a parse w/o modifying the text being parsed. + * + * **Haskell equivalent:** + * - Applicative#pure() + * - Monad#return() + */ +fun pure(value: T): Parser = { text -> + listOf(ParseResult(value, text)) +} + +/** + * Returns a parser indicating failure which will fail all parsers applied after it. + */ +fun fail(): Parser = { emptyList() } +// endregion + +/** + * Applies a parser and invokes the parser with parsed value if it was successful. + * In case of multiple successful parsing returned + * from this parse or next parser, they're flattened. + * + * **FP equivalent:** + * - Monad#bind + * - Scala's flatMap{} + * + * **Example:** + * ``` + * // parse the text "Jetpack Compose" or "Jetpack+Compose" + * fun jetpackComposeParser() = string("Jetpack").apply { jetpack -> + * (char(' ') or char('+')).apply { //ignored divider + * string("Compose").apply { compose -> + * pure(jetpack + compose) + * } + * } + * } + * ``` + * + * @receiver the first parser to apply _(Parser 1)_. + * @param nextParser a function creating the next parser which will be applied only if + * _Parser 1_ was successful. + * @return a new parser that chains _"Parser 1 -> Parser 2"_. + * + */ +fun Parser.apply( + nextParser: (T) -> Parser +): Parser = { string -> + val res1 = this(string) // apply parser 1 + + /* + * Parser 1 = "this" + * Parser 2 = "nextParser" + * # Case A: + * Parser 1 fails, meaning it returns res1 = [] + * => Parser 2 won't be invoked, [] (failure) is returned + * + * # Case B: + * Parser 1 parses only one thing: res1 = [ParseResult] + * => Parser 2 will be invoked only once, [f(ParseResult)] + * + * # Case C: + * Parser 1 parses multiple things: res1 = [pr1, pr2, ... , prn] + * => Parser 2 (`f`) will be invoked N times. + * If Parser 2 also returns multiple results they'll be flattened and returned [n*m] + * where n = Parser 1 results and m = Parser 2 results for each n. + */ + res1.flatMap { + // Apply Parser 2 to each successfully parsed value by Parser 1 and its leftover + nextParser(it.value).invoke(it.leftover) + } +} + +// region Read a not parsed character +/** + * A parser that reads one character from the text left to parse. + * Fails if the text is empty. + */ +fun item(): Parser = { string -> + if (string.isNotEmpty()) { + // return the first character as value and the rest as leftover + listOf( + ParseResult( + value = string.first(), + leftover = string.drop(1) + ) + ) + } else emptyList() +} +// endregion + +// region Core: Parse char, string & a symbol satisfying a predicate +/** + * Parses a char if it satisfies a given predicate. + * @param predicate returns whether the parsing is successful. + * @return a parser that parses a character for a predicate. + */ +fun sat(predicate: (Char) -> Boolean): Parser = { string -> + item().apply { char -> + if (predicate(char)) pure(char) else fail() + }.invoke(string) +} + +/** + * Parses a specific character. + * @param c the character to parse + * @return a parser that parses a character + */ +fun char(c: Char): Parser = sat { it == c } + +fun string(str: String): Parser = { string -> + if (str.isEmpty()) pure("").invoke(string) else { + // recurse + char(str.first()).apply { c -> + string(str.drop(1)).apply { cs -> + pure(c + cs) + } + }.invoke(string) + } +} +// endregion \ No newline at end of file diff --git a/parser/src/main/java/com/ivy/parser/RecursionCombinators.kt b/parser/src/main/java/com/ivy/parser/RecursionCombinators.kt new file mode 100644 index 0000000000..d25c5eed73 --- /dev/null +++ b/parser/src/main/java/com/ivy/parser/RecursionCombinators.kt @@ -0,0 +1,127 @@ +package com.ivy.parser + +/** + * Builds a new parser that do **Parser 1 || Parser 2**. Tries _Parser 1_ and + * if it succeeds returns its result. If _Parser 1_ fails executes _Parser 2_. + * Left associative operator for chaining parsers. + * + * **Example** + * ``` + * // parser Calculator operation + * enum Operation { Plus, Minus, Multiple, Divide } + * fun operationParser(): Parser = + * (char('+') or char('-') or char('*') or char('-')).apply { opSymbol -> + * when(opSymbol) { + * '+' -> Operation.Plus + * '-' -> Operation.Minus + * '*' -> Operation.Multiple + * '/' -> Operation.Divide + * else -> error("should NOT happen!") + * } + * } + * ``` + * + * @receiver the first parser to apply _(Parser 1)_. + * @param parser2 the second parser to apply _(Parser 2)_. + * @return a combined OR parser: _Parser 1_ **||** _Parser 2_. + */ +infix fun Parser.or(parser2: Parser): Parser = { text -> + this(text).ifEmpty { parser2(text) } +} + +/** + * Applies _Parser 1_ then _Parser 2_ and returns their results combined. + * + * **Example:** + * ``` + * fun parseAsciiA(): Parser = char('A').apply { char -> + * char.toByte().toInt() + * } + * + * fun combined(): Parser = char('A') + parseAsciiA() + * // ['A', 65] + * ``` + * + * @receiver _Parser 1_ + * @param parser2 _Parser 2_ + * @return the combined result or Parser 1 + Parser 2: [[Parser 1]] + [[Parser 2]] + */ +operator fun Parser.plus(parser2: Parser): Parser = { text -> + this(text) + parser2(text) +} + +/** + * Takes only the first variation of a parsing. + * Parsers always return a list of results which may contain more than one parsings. + * @return a parser that: + * **[[ParserRes1, ParserRes2, ParserResN]] => [[ParserRes1]]** + */ +fun Parser.first(): Parser = { text -> + val res = this(text) + res.take(1) +} + +// region Occurrences +/** + * Zero or many occurrences of a parser. + */ +fun zeroOrMany(parser: Parser): Parser> { + fun oneOrMany(parser: Parser): Parser> = + parser.apply { one -> // this recursion will stop when "one" stops returning + zeroOrMany(parser).apply { zeroOrMany -> + pure(listOf(one) + zeroOrMany) + } + } + + // If "oneOrMany" fails to parse, a.k.a returns failure [] + // then to hold the "zero" part true, return a successful parsing of an empty list of T + val allVariations = oneOrMany(parser) + pure(emptyList()) + + // this recursion returns an array of all occurrences of the parsed value + // example: zeroMany(char('a')).invoke("aaa") will return: + // [ParseResult(value=[a, a, a], leftover=), ParseResult(value=[a, a], leftover=), + // ParseResult(value=[a], leftover=aa), ParseResult(value=[], leftover=aaa)] + // => we need to take only the most result with the most occurrences + // which happens to be at index 0 or first + return allVariations.first() +} + +/** + * One or many occurrences of a parser. + */ +fun oneOrMany(parser: Parser): Parser> = parser.apply { one -> + // parsed one occurrence successfully + zeroOrMany(parser).apply { zeroOrMany -> + pure(listOf(one) + zeroOrMany) + } +} + +/** + * Zero or one occurrences of a parser. This operation cannot fail. + */ +fun optional(parser: Parser): Parser = { text -> + val result = parser(text) + // if the parser fails it returns empty result + // in case of failure to satisfy "zero" return a successful null result + result.ifEmpty { listOf(ParseResult(null, text)) } +} +// endregion + +/** + * Returns a list of T values separated by something. + * This operation will never fail. In case of failure will simple return a value empty list. + */ +fun Parser.separatedBy(separator: Parser): Parser> { + fun Parser.oneOrManySepBy(separator: Parser): Parser> = this.apply { one -> + zeroOrMany( + separator.apply { + this + } + ).apply { manySeparated -> + pure(listOf(one) + manySeparated) + } + } + // the same pattern as in "zeroOrMany" + val allVariations = this.oneOrManySepBy(separator) + pure(emptyList()) + return allVariations.first() +} \ No newline at end of file diff --git a/parser/src/main/java/com/ivy/parser/common/AlphabetParser.kt b/parser/src/main/java/com/ivy/parser/common/AlphabetParser.kt new file mode 100644 index 0000000000..d5265df085 --- /dev/null +++ b/parser/src/main/java/com/ivy/parser/common/AlphabetParser.kt @@ -0,0 +1,6 @@ +package com.ivy.parser.common + +import com.ivy.parser.Parser +import com.ivy.parser.sat + +fun letter(): Parser = sat { it.isLetter() } \ No newline at end of file diff --git a/parser/src/main/java/com/ivy/parser/common/NumberParser.kt b/parser/src/main/java/com/ivy/parser/common/NumberParser.kt new file mode 100644 index 0000000000..77da510b7d --- /dev/null +++ b/parser/src/main/java/com/ivy/parser/common/NumberParser.kt @@ -0,0 +1,54 @@ +package com.ivy.parser.common + +import com.ivy.parser.* + +fun digit(): Parser = sat { it.isDigit() } + +/** + * Parses an integer number without a sign. + */ +fun int(): Parser = oneOrMany(digit()).apply { digits -> + val number = try { + digits.joinToString(separator = "").toInt() + } catch (e: Exception) { + Int.MAX_VALUE + } + pure(number) +} + +/** + * Parses a decimal number from as a string as double. + * + * **Supported formats:** + * - 3.14, 1024.0 _"#.#"_ + * - .5, .9 _".#"_ + * - "3." 15. _"#."_ + * - 3, 5, 8 _"#"_ + */ +fun number(): Parser { + fun oneOrMoreDigits(): Parser = oneOrMany(digit()).apply { digits -> + pure(digits.joinToString(separator = "")) + } + + return int().apply { intPart -> + // 3.14, ###.00 + char('.').apply { + oneOrMoreDigits().apply { decimalPart -> + pure("$intPart.$decimalPart".toDouble()) + } + } + } or char('.').apply { + // .5 => 0.5 + oneOrMoreDigits().apply { decimalPart -> + pure("0.$decimalPart".toDouble()) + } + } or int().apply { intPart -> + // 3. => 3.0 + char('.').apply { + pure(intPart.toDouble()) + } + } or int().apply { + // 3, 5, 13 + pure(it.toDouble()) + } +} \ No newline at end of file diff --git a/parser/src/test/java/com/ivy/parser/AlphabetParserTest.kt b/parser/src/test/java/com/ivy/parser/AlphabetParserTest.kt new file mode 100644 index 0000000000..2b68022715 --- /dev/null +++ b/parser/src/test/java/com/ivy/parser/AlphabetParserTest.kt @@ -0,0 +1,32 @@ +package com.ivy.parser + +import com.ivy.parser.common.letter +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.char +import io.kotest.property.checkAll +import io.kotest.property.exhaustive.exhaustive + +class AlphabetParserTest : FreeSpec({ + "parses a letter" { + val allLetters = ('a'..'z') + ('A'..'Z') + checkAll(allLetters.exhaustive()) { letter -> + val parser = letter() + + val res = parser(letter.toString()) + + res shouldBe listOf(ParseResult(letter, "")) + } + } + + "fails to parse a non-letter" { + checkAll(Arb.char(listOf('!'..'0', '{'..'~'))) { nonLetter -> + val parser = letter() + + val res = parser(nonLetter.toString()) + + res shouldBe emptyList() + } + } +}) \ No newline at end of file diff --git a/parser/src/test/java/com/ivy/parser/LexicalCombinatorsTest.kt b/parser/src/test/java/com/ivy/parser/LexicalCombinatorsTest.kt new file mode 100644 index 0000000000..07d9d8f6b8 --- /dev/null +++ b/parser/src/test/java/com/ivy/parser/LexicalCombinatorsTest.kt @@ -0,0 +1,63 @@ +package com.ivy.parser + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class LexicalCombinatorsTest : FreeSpec({ + "parses whitespace" - { + withData( + nameFn = { (text, value, leftover) -> + "in \"$text\" text as ${value.map { "'$it'" }} with \"$leftover\" leftover" + }, + // Text (as) Whitespace values (with) Leftover + row(" Iliyan", listOf(' '), "Iliyan"), + row(" a b c", listOf(' ', ' ', ' '), "a b c"), + row("", listOf(), ""), + row("\n\n1", listOf('\n', '\n'), "1"), + row("\tOkay\t", listOf('\t'), "Okay\t"), + ) { (text, value, leftover) -> + val parser = whitespace() + + val res = parser(text) + + res shouldBe listOf(ParseResult(value, leftover)) + } + } + + "parses symbolic token" - { + withData( + nameFn = { (token, text, leftover) -> + "\"$token\" in text \"$text\" with \"$leftover\" leftover" + }, + // Token (in) Text (with) Leftover + row("abc", "abc text", "text"), + row("okay", "okay\tnice", "nice"), + ) { (token, text, leftover) -> + val parser = symbolicToken(token) + + val res = parser(text) + + res shouldBe listOf(ParseResult(token, leftover)) + } + } + + "fails to parses symbolic token" - { + withData( + nameFn = { (token, text) -> + "\"$token\" in text \"$text\"" + }, + // Token (in) Text + row("test", ""), + row("okay", "ok\tnice"), + row("cool", " cool"), + ) { (token, text) -> + val parser = symbolicToken(token) + + val res = parser(text) + + res shouldBe emptyList() + } + } +}) \ No newline at end of file diff --git a/parser/src/test/java/com/ivy/parser/NumberParserTest.kt b/parser/src/test/java/com/ivy/parser/NumberParserTest.kt new file mode 100644 index 0000000000..90519208be --- /dev/null +++ b/parser/src/test/java/com/ivy/parser/NumberParserTest.kt @@ -0,0 +1,94 @@ +package com.ivy.parser + +import com.ivy.parser.common.int +import com.ivy.parser.common.number +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class NumberParserTest : FreeSpec({ + // region Parse integer + "parses an integer" - { + withData( + nameFn = { (text, number, leftover) -> + "from text \"$text\" as $number with \"$leftover\" leftover" + }, + // Text (as) Number (with) Leftover + row("0", 0, ""), + row("1", 1, ""), + row("123456", 123456, ""), + row("900ok", 900, "ok"), + row("5+10", 5, "+10"), + row("01070 ", 1070, " "), + ) { (text, number, leftover) -> + val parser = int() + + val res = parser(text) + + res shouldBe listOf(ParseResult(number, leftover)) + } + } + + "fails to parse an integer" - { + withData( + nameFn = { (text) -> "from \"$text\" text" }, + row(" 3"), + row("*10"), + row("=8"), + ) { (text) -> + val parser = int() + + val res = parser(text) + + res shouldBe emptyList() + } + } + // endregion + + // region Parse double + "parses a decimal" - { + withData( + nameFn = { (text, double, leftover) -> + "from \"$text\" text as $double with \"$leftover\" leftover" + }, + // (from) Text (as) Double (with) Leftover + row("0", 0.0, ""), + row("3.14", 3.14, ""), + row(".003", 0.003, ""), + row(".5", 0.5, ""), + row("1024wtf?", 1_024.0, "wtf?"), + row("0.99", 0.99, ""), + row("5.65+18", 5.65, "+18"), + row("3.%*10", 3.0, "%*10"), + row("7", 7.0, ""), + row("5748b", 5748.0, "b"), + row("1", 1.0, ""), + ) { (text, double, leftover) -> + val parser = number() + + val res = parser(text) + + res shouldBe listOf(ParseResult(double, leftover)) + } + } + + "fails to parse a decimal" - { + withData( + nameFn = { (text) -> "from \"$text\" text" }, + // Text + row(" 3.14"), + row("a10"), + row("..3"), + row(""), + row("."), + ) { (text) -> + val parser = number() + + val res = parser(text) + + res shouldBe emptyList() + } + } + // endregion +}) \ No newline at end of file diff --git a/parser/src/test/java/com/ivy/parser/ParserTest.kt b/parser/src/test/java/com/ivy/parser/ParserTest.kt new file mode 100644 index 0000000000..389d3e3e2a --- /dev/null +++ b/parser/src/test/java/com/ivy/parser/ParserTest.kt @@ -0,0 +1,84 @@ +package com.ivy.parser + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class ParserTest : FreeSpec({ + // region Parse Char + "parses char" - { + withData( + // Char (in) Text (with) Leftover + nameFn = { (char, text, leftover) -> + "'$char' in text \"$text\" with \"$leftover\" leftover" + }, + row('a', "a", ""), + row('b', "back", "ack"), + row('T', "T T", " T"), + ) { (char, text, leftover) -> + val parser = char(char) + + val res = parser(text) + + res shouldBe listOf(ParseResult(char, leftover)) + } + } + + "fails to parse char" - { + withData( + nameFn = { (char, text) -> + "'$char' in text \"$text\"" + }, + // Char (in) Text + row('a', ""), + row('b', "a"), + row('c', "ac"), + ) { (char, text) -> + val parser = char(char) + + val res = parser(text) + + res shouldBe emptyList() + } + } + // endregion + + // region Parse String + "parses string" - { + withData( + nameFn = { (str, text, leftover) -> + "\"$str\" in text \"$text\" with \"$leftover\" leftover " + }, + // String (in) Text (with) Leftover + row("aba", "aba", ""), + row("okay", "okay Google", " Google"), + row("zZ", "zZZz", "Zz"), + ) { (str, text, leftover) -> + val parser = string(str) + + val res = parser(text) + + res shouldBe listOf(ParseResult(str, leftover)) + } + } + + "fails to parse string" - { + withData( + nameFn = { (str, text) -> + "\"$str\" in text \"$text\"" + }, + // String (in) Text + row("cat", "car"), + row("Test", "test"), + row("Itworks!", "It works!") + ) { (str, text) -> + val parser = string(str) + + val res = parser(text) + + res shouldBe emptyList() + } + } + // endregion +}) \ No newline at end of file diff --git a/parser/src/test/java/com/ivy/parser/RecursionCombinatorsTest.kt b/parser/src/test/java/com/ivy/parser/RecursionCombinatorsTest.kt new file mode 100644 index 0000000000..55d985aae5 --- /dev/null +++ b/parser/src/test/java/com/ivy/parser/RecursionCombinatorsTest.kt @@ -0,0 +1,46 @@ +package com.ivy.parser + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class RecursionCombinatorsTest : FreeSpec({ + "parses zero or one characters" - { + withData( + nameFn = { (char, text, value, leftover) -> + "'$char' in text \"$text\" as value '$value' with '$leftover' leftover" + }, + // Char (in) Text (as) Value (with) Leftover + row('a', "aaa", 'a', "aa"), + row('b', "", null, ""), + row('c', "cool", 'c', "ool"), + row('=', "5+5", null, "5+5"), + ) { (c, text, value, leftover) -> + val parser = optional(char(c)) + + val res = parser(text) + + res shouldBe listOf(ParseResult(value, leftover)) + } + } + + "parses items separated by" - { + withData( + nameFn = { (separator, text, values, leftover) -> + "'$separator' sep in text \"$text\" as values $values with \"$leftover\" leftover" + }, + // Separator (in) Text (as) [Values] (with) Leftover + row(",", "a,b,c,d", listOf('a', 'b', 'c', 'd'), ""), + row("--", "a--b--c:test", listOf('a', 'b', 'c'), ":test"), + row("--", "", listOf(), ""), + row(" ", "okay", listOf('o'), "kay"), + ) { (separator, text, values, leftover) -> + val parser = item().separatedBy(string(separator)) + + val res = parser(text) + + res shouldBe listOf(ParseResult(values, leftover)) + } + } +}) \ No newline at end of file diff --git a/pie-charts/README.md b/pie-charts/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/pie-charts/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/pie-charts/build.gradle.kts b/pie-charts/build.gradle.kts deleted file mode 100644 index d2834c75ae..0000000000 --- a/pie-charts/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":core:data-model")) - implementation(project(":temp-domain")) - implementation(project(":navigation")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":temp-persistence")) - implementation(project(":ui-components-old")) - -} \ No newline at end of file diff --git a/pie-charts/src/main/AndroidManifest.xml b/pie-charts/src/main/AndroidManifest.xml deleted file mode 100644 index 2664ad951d..0000000000 --- a/pie-charts/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/PieChart.kt b/pie-charts/src/main/java/com/ivy/pie_charts/PieChart.kt deleted file mode 100644 index 91ece81b61..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/PieChart.kt +++ /dev/null @@ -1,302 +0,0 @@ -package com.ivy.pie_charts - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.RectF -import android.view.MotionEvent -import android.view.View -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -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.toArgb -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview -import com.ivy.pie_charts.model.CategoryAmount -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.utils.convertDpToPixel -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.timeNowUTC -import com.ivy.wallet.utils.toEpochMilli -import timber.log.Timber -import kotlin.math.acos -import kotlin.math.sqrt - - -const val PIE_CHART_RADIUS_DP = 128 -const val RADIUS_DP = 112f - -@Composable -fun PieChart( - type: TrnTypeOld, - categoryAmounts: List, - selectedCategory: SelectedCategory?, - - onCategoryClicked: (CategoryOld?) -> Unit = {} -) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - AndroidView( - modifier = Modifier - .size((PIE_CHART_RADIUS_DP * 2).dp) - .drawColoredShadow( - color = Black, - alpha = if (UI.colors.isLight) 0.05f else 0.5f, - offsetY = 32.dp, - shadowRadius = 48.dp - ) - .clip(CircleShape) - .background( - brush = Gradient( - UI.colors.medium, - UI.colors.pure - ).asVerticalBrush(), - shape = CircleShape - ) - .padding(all = 16.dp), - factory = { - PieChartView(it) - }, - update = { view -> - view.display( - categoryAmounts = categoryAmounts, - selectedCategory = selectedCategory, - onCategoryClicked = onCategoryClicked - ) - } - ) - - IvyIcon( - modifier = Modifier - .size(100.dp) - .clip(CircleShape) - .background(UI.colors.medium) - .padding(all = 20.dp), - icon = if (type == TrnTypeOld.INCOME) R.drawable.ic_income else R.drawable.ic_expense, - tint = Gray - ) - } - -} - -private class PieChartView(context: Context) : View(context) { - private var categoryAmounts = emptyList() - private var paints = mapOf() - private var totalAmount = 0.0 - - val rectangle = RectF( - 0f, 0f, - convertDpToPixel(context, 2 * RADIUS_DP), - convertDpToPixel(context, 2 * RADIUS_DP) - ) - - var onCategoryClicked: (CategoryOld?) -> Unit = {} - - fun display( - categoryAmounts: List, - selectedCategory: SelectedCategory?, - onCategoryClicked: (CategoryOld?) -> Unit - ) { - this.onCategoryClicked = onCategoryClicked - - this.categoryAmounts = categoryAmounts - this.totalAmount = categoryAmounts.sumOf { it.amount } - - this.paints = categoryAmounts - .map { - val category = it.category - val categoryColor = category?.color?.toComposeColor() ?: Gray - val color = if (selectedCategory == null) { - categoryColor - } else { - if (selectedCategory.category == category) { - categoryColor - } else { - categoryColor.copy( - alpha = 0.15f - ) - } - } - - category to paintFor( - color = color - ) - } - .toMap() - - invalidate() - } - - private fun paintFor(color: Color): Paint { - return Paint().apply { - this.color = color.toArgb() - this.strokeWidth = convertDpToPixel(context, 2f) - this.strokeCap = Paint.Cap.ROUND - this.strokeJoin = Paint.Join.ROUND - this.isAntiAlias = true - } - } - - private val zones = mutableListOf() - - override fun onDraw(canvas: Canvas) { - var startAngle = -90.0 - - zones.clear() - - for (categoryAmount in categoryAmounts) { - val paint = paints[categoryAmount.category] ?: continue - val amount = categoryAmount.amount - - if (amount == 0.0) continue - - val percent = amount / totalAmount - val sweepAngle = 360 * percent - - zones.add( - Zone( - startAngle = startAngle, - endAngle = startAngle + sweepAngle, - category = categoryAmount.category - ) - ) - - canvas.drawArc( - rectangle, - startAngle.toFloat(), - sweepAngle.toFloat(), - true, - paint - ) //draw - - startAngle += sweepAngle - } - } - - - private var startClickTime = 0L - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - startClickTime = timeNowUTC().toEpochMilli() - } - MotionEvent.ACTION_UP -> { - val clickDuration: Long = timeNowUTC().toEpochMilli() - startClickTime - if (clickDuration < MAX_CLICK_DURATION) { - val touchX = event.x - val touchY = event.y - - val centerX = width / 2f - val centerY = height / 2f - Timber.d("click: x = $touchX, y = $touchY (width = $width, height = $height)") - - val angle = getAngle( - touchX = touchX, - touchY = touchY, - centerX = centerX, - centerY = centerY - ) - - Timber.d("degrees = $angle") - - val clickedCategory = zones - .firstOrNull { zone -> - zone.contains(angle = angle) - }?.category - Timber.i("clicked category = ${clickedCategory?.name}") - - onCategoryClicked(clickedCategory) - } - } - } - return true - } - - private fun getAngle( - touchX: Float, - touchY: Float, - centerX: Float, - centerY: Float - ): Double { - val angle: Double - val x2 = touchX - centerX - val y2 = touchY - centerY - val d1 = sqrt((centerY * centerY).toDouble()) - val d2 = sqrt((x2 * x2 + y2 * y2).toDouble()) - angle = if (touchX >= centerX) { - Math.toDegrees(acos((-centerY * y2) / (d1 * d2))) - } else - 360 - Math.toDegrees(acos((-centerY * y2) / (d1 * d2))) - return angle - 90.0 - } - - companion object { - private const val MAX_CLICK_DURATION = 200 - } - - private data class Zone( - val startAngle: Double, - val endAngle: Double, - val category: CategoryOld? - ) { - fun contains(angle: Double): Boolean = - angle > startAngle && angle < endAngle - } -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - Column( - Modifier.fillMaxSize() - ) { - Spacer(Modifier.weight(1f)) - - PieChart( - type = TrnTypeOld.EXPENSE, - categoryAmounts = listOf( - CategoryAmount( - category = CategoryOld("Bills", Green.toArgb()), - amount = 791.0 - ), - CategoryAmount( - category = CategoryOld("Shisha", Green.toArgb()), - amount = 411.93 - ), - CategoryAmount( - category = CategoryOld("Food & Drink", IvyDark.toArgb()), - amount = 260.03 - ), - CategoryAmount( - category = CategoryOld("Gifts", RedLight.toArgb()), - amount = 160.0 - ), - CategoryAmount( - category = null, - amount = 497.0 - ), - ), - selectedCategory = null - ) - - Spacer(Modifier.weight(1f)) - } - } -} \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticBottomBar.kt b/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticBottomBar.kt deleted file mode 100644 index c53e1580e6..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticBottomBar.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.ivy.pie_charts - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.GradientGreen -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.components.ActionsRow -import com.ivy.wallet.ui.theme.components.CloseButton -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.gradientCutBackgroundTop -import com.ivy.wallet.utils.navigationBarInset -import com.ivy.wallet.utils.toDensityDp - -@Composable -fun BoxWithConstraintsScope.PieChartStatisticBottomBar( - type: TrnTypeOld, - bottomInset: Dp = navigationBarInset().toDensityDp(), - onClose: () -> Unit, - onAdd: (TrnTypeOld) -> Unit -) { - ActionsRow( - modifier = Modifier - .align(Alignment.BottomCenter) - .gradientCutBackgroundTop() - .padding(bottom = bottomInset) - .padding(bottom = 16.dp) - ) { - Spacer(Modifier.width(20.dp)) - - CloseButton { - onClose() - } - - Spacer(Modifier.weight(1f)) - - val isIncome = type == TrnTypeOld.INCOME - IvyButton( - iconStart = R.drawable.ic_plus, - text = if (isIncome) stringResource(id = R.string.add_income) else stringResource(id = R.string.add_expense), - backgroundGradient = if (isIncome) GradientGreen else Gradient.solid(UI.colorsInverted.pure), - textStyle = UI.typo.b2.style( - color = if (isIncome) White else UI.colors.pure, - fontWeight = FontWeight.ExtraBold - ), - iconTint = if (isIncome) White else UI.colors.pure - ) { - onAdd(type) - } - - Spacer(Modifier.width(20.dp)) - } -} - -@Preview -@Composable -private fun PreviewBottomBar() { - IvyPreview { - PieChartStatisticBottomBar( - type = TrnTypeOld.INCOME, - bottomInset = 16.dp, - onAdd = {}, - onClose = {} - ) - } -} \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticEvent.kt b/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticEvent.kt deleted file mode 100644 index 961c33775d..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticEvent.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.ivy.pie_charts - -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.CategoryOld -import com.ivy.pie_charts.model.CategoryAmount - -sealed class PieChartStatisticEvent { - data class Start(val screen: Unit) : PieChartStatisticEvent() - object OnSelectNextMonth : PieChartStatisticEvent() - - object OnSelectPreviousMonth : PieChartStatisticEvent() - - data class OnSetPeriod(val timePeriod: TimePeriod) : PieChartStatisticEvent() - - data class OnCategoryClicked(val category: CategoryOld?) : - PieChartStatisticEvent() - - data class OnShowMonthModal(val timePeriod: TimePeriod?) : PieChartStatisticEvent() - - data class OnUnpackSubCategories(val unpackAllSubCategories: Boolean) : PieChartStatisticEvent() - - data class OnSubCategoryListExpanded( - val parentCategoryAmount: CategoryAmount, - val expandedState: Boolean - ) : PieChartStatisticEvent() -} \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticScreen.kt b/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticScreen.kt deleted file mode 100644 index e08de48360..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticScreen.kt +++ /dev/null @@ -1,609 +0,0 @@ -package com.ivy.pie_charts - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - - -import com.ivy.pie_charts.model.CategoryAmount -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.* -import com.ivy.wallet.ui.theme.modal.ChoosePeriodModal -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1Row -import com.ivy.wallet.utils.* - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.PieChartStatisticScreen( -) { - val viewModel: PieChartStatisticViewModel = hiltViewModel() - val state by viewModel.state().collectAsState() - - - UI( - state = state, - onEventHandler = viewModel::onEvent - ) -} - -@ExperimentalFoundationApi -@Composable -private fun BoxWithConstraintsScope.UI( - state: PieChartStatisticState = PieChartStatisticState(), - onEventHandler: (PieChartStatisticEvent) -> Unit = {} -) { - - val lazyState = rememberLazyListState() - val expanded by remember { derivedStateOf { lazyState.firstVisibleItemIndex < 1 } } - val percentExpanded by animateFloatAsState( - targetValue = if (expanded) 1f else 0f, - animationSpec = springBounce() - ) - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding(), - state = lazyState - ) { - stickyHeader { - Header( - transactionType = state.transactionType, - period = state.period, - percentExpanded = percentExpanded, - - currency = state.baseCurrency, - amount = state.totalAmount, - - onShowMonthModal = { - onEventHandler(PieChartStatisticEvent.OnShowMonthModal(state.period)) - }, - onSelectNextMonth = { - onEventHandler(PieChartStatisticEvent.OnSelectNextMonth) - }, - onSelectPreviousMonth = { - onEventHandler(PieChartStatisticEvent.OnSelectPreviousMonth) - }, - showCloseButtonOnly = state.showCloseButtonOnly, - - onClose = { - - }, - onAdd = { trnType -> -// nav.navigateTo( -// EditTransaction( -// initialTransactionId = null, -// type = trnType -// ) -// ) - } - ) - } - - item { - Spacer(Modifier.height(20.dp)) - - Text( - modifier = Modifier - .padding(start = 32.dp) - .testTag("piechart_title"), - text = if (state.transactionType == TrnTypeOld.EXPENSE) stringResource(R.string.expenses) else stringResource( - R.string.income - ), - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - BalanceRow( - modifier = Modifier - .padding(start = 32.dp, end = 16.dp) - .testTag("piechart_total_amount") - .alpha(percentExpanded), - currency = state.baseCurrency, - balance = state.totalAmount, - currencyUpfront = false, - currencyFontSize = 30.sp - ) - } - - if (state.showUnpackOption) { - item { - IvyCheckboxWithText( - modifier = Modifier - .padding(top = 12.dp, start = 16.dp), - text = stringResource(R.string.unpack_all_subcategories), - checked = state.unpackAllSubCategories - ) { - onEventHandler( - PieChartStatisticEvent.OnUnpackSubCategories( - unpackAllSubCategories = !state.unpackAllSubCategories - ) - ) - } - } - } - - item { - Spacer(Modifier.height(40.dp)) - - PieChart( - type = state.transactionType, - categoryAmounts = state.pieChartCategoryAmount, - selectedCategory = state.selectedCategory, - onCategoryClicked = { clickedCategory -> - onEventHandler(PieChartStatisticEvent.OnCategoryClicked(clickedCategory)) - } - ) - - Spacer(Modifier.height(48.dp)) - } - - itemsIndexed( - items = state.categoryAmounts - ) { index, item -> - if (item.totalAmount() != 0.0) { - if (index != 0) { - Spacer(Modifier.height(16.dp)) - } - CategoryAmountCardWithSub( - categoryAmount = item, - currency = state.baseCurrency, - totalAmount = state.totalAmount, - selectedCategory = state.selectedCategory, - - state = state, - onEventHandler = onEventHandler - ) - } - } - - item { - Spacer(Modifier.height(160.dp)) //scroll hack - } - } - - ChoosePeriodModal( - modal = state.choosePeriodModal, - dismiss = { - onEventHandler(PieChartStatisticEvent.OnShowMonthModal(null)) - } - ) { - onEventHandler(PieChartStatisticEvent.OnSetPeriod(it)) - } -} - -@Composable -private fun Header( - transactionType: TrnTypeOld, - period: TimePeriod, - percentExpanded: Float, - - currency: String, - amount: Double, - showCloseButtonOnly: Boolean = false, - - - onShowMonthModal: () -> Unit, - onSelectNextMonth: () -> Unit, - onSelectPreviousMonth: () -> Unit, - - onClose: () -> Unit, - onAdd: (TrnTypeOld) -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(pureBlur()) - .statusBarsPadding() - .padding(top = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - CloseButton { - onClose() - } - - //Balance mini row - if (percentExpanded < 1f) { - Spacer(Modifier.width(12.dp)) - - BalanceRowMini( - modifier = Modifier - .alpha(1f - percentExpanded), - currency = currency, - balance = amount, - ) - } - - if (!showCloseButtonOnly) { - Spacer(Modifier.weight(1f)) - - IvyOutlinedButton( - modifier = Modifier.horizontalSwipeListener( - sensitivity = 75, - onSwipeLeft = { - onSelectNextMonth() - }, - onSwipeRight = { - onSelectPreviousMonth() - } - ), - iconStart = R.drawable.ic_calendar, - text = period.toDisplayShort(1), - ) { - onShowMonthModal() - } - - if (percentExpanded > 0f) { - Spacer(Modifier.width(12.dp)) - - val backgroundGradient = if (transactionType == TrnTypeOld.EXPENSE) - gradientExpenses() else GradientGreen - CircleButtonFilledGradient( - modifier = Modifier - .thenIf(percentExpanded == 1f) { - drawColoredShadow(backgroundGradient.startColor) - } - .alpha(percentExpanded) - .size(lerp(1, 40, percentExpanded).dp), - iconPadding = 4.dp, - icon = R.drawable.ic_plus, - backgroundGradient = backgroundGradient, - tint = if (transactionType == TrnTypeOld.EXPENSE) - UI.colors.pure else White - ) { - onAdd(transactionType) - } - } - - Spacer(Modifier.width(20.dp)) - } - } -} - -@Composable -private fun CategoryAmountCardWithSub( - categoryAmount: CategoryAmount, - currency: String, - totalAmount: Double, - - selectedCategory: SelectedCategory?, - - state: PieChartStatisticState, - - onEventHandler: (PieChartStatisticEvent) -> Unit = {}, -) { - CategoryAmountCard( - categoryAmount = categoryAmount, - currency = currency, - totalAmount = totalAmount, - selectedCategory = selectedCategory, - onSubCategoryListExpand = { - onEventHandler( - PieChartStatisticEvent.OnSubCategoryListExpanded( - categoryAmount, - !categoryAmount.subCategoryState.subCategoryListExpanded - ) - ) - } - ) { -// nav.navigateTo( -// ItemStatistic( -// categoryId = categoryAmount.category?.id, -// unspecifiedCategory = categoryAmount.isCategoryUnspecified, -// accountIdFilterList = state.accountIdFilterList, -// transactions = categoryAmount.associatedTransactions -// ) -// ) - } - if (categoryAmount.subCategoryState.subCategoryListExpanded) { - Column(modifier = Modifier.padding(start = 24.dp)) { - categoryAmount.subCategoryState.subCategoriesList.forEach { - Spacer(Modifier.height(16.dp)) - CategoryAmountCard( - categoryAmount = it, - currency = currency, - totalAmount = totalAmount, - selectedCategory = selectedCategory - ) { -// nav.navigateTo( -// ItemStatistic( -// categoryId = it.category?.id, -// unspecifiedCategory = it.isCategoryUnspecified, -// accountIdFilterList = state.accountIdFilterList, -// transactions = it.associatedTransactions -// ) -// ) - } - } - } - } -} - -@Composable -private fun CategoryAmountCard( - categoryAmount: CategoryAmount, - currency: String, - totalAmount: Double, - - selectedCategory: SelectedCategory?, - - onSubCategoryListExpand: () -> Unit = {}, - onClick: () -> Unit -) { - val category = categoryAmount.category - val amount = categoryAmount.getRelevantAmount() - - val categoryColor = category?.color?.toComposeColor() ?: Gray //Unspecified category = Gray - val selectedState = when { - selectedCategory == null -> { - //no selectedCategory - false - } - categoryAmount.category == selectedCategory.category -> { - //selectedCategory && we're selected - true - } - else -> false - } - val backgroundColor = if (selectedState) categoryColor else UI.colors.medium - - val textColor = findContrastTextColor( - backgroundColor = backgroundColor - ) - - Row( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .thenIf(selectedState) { - drawColoredShadow(backgroundColor) - } - .clip(UI.shapes.rounded) - .background(backgroundColor, UI.shapes.rounded) - .clickable { - onClick() - } - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - ItemIconM( - modifier = Modifier.background(categoryColor, CircleShape), - iconName = category?.icon, - tint = findContrastTextColor(categoryColor), - iconContentScale = ContentScale.None, - Default = { - ItemIconMDefaultIcon( - modifier = Modifier.background(categoryColor, CircleShape), - iconName = category?.icon, - defaultIcon = R.drawable.ic_custom_category_m, - tint = findContrastTextColor(categoryColor) - ) - } - ) - - - - Spacer(Modifier.width(16.dp)) - - Column( - modifier = Modifier.weight(1f) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier - .weight(1f) - .padding(end = 16.dp), - text = category?.name ?: stringResource(R.string.unspecified), - style = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Start - ) - ) - - PercentText( - amount = amount, - totalAmount = totalAmount, - selectedState = selectedState, - contrastColor = textColor - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.height(4.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .weight(1f) - ) { - AmountCurrencyB1Row( - amount = amount, - currency = currency, - textColor = textColor, - amountFontWeight = FontWeight.ExtraBold - ) - } - if (categoryAmount.subCategoryState.subCategoriesList.isNotEmpty()) { - IvyIcon( - modifier = Modifier - .padding(end = 16.dp) - .clickable { - onSubCategoryListExpand() - }, - icon = R.drawable.ic_expandarrow, - tint = findContrastTextColor(categoryColor) - ) - } - } - } - } -} - -@Composable -private fun PercentText( - amount: Double, - totalAmount: Double, - selectedState: Boolean, - contrastColor: Color -) { - Text( - text = if (totalAmount != 0.0) - stringResource(R.string.percent, ((amount / totalAmount) * 100).format(2)) - else stringResource(R.string.percent, "0"), - style = UI.typoSecond.b2.style( - color = if (selectedState) contrastColor else UI.colorsInverted.pure, - fontWeight = FontWeight.Normal - ) - ) -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview_Expense() { - IvyPreview { - val state = PieChartStatisticState( - transactionType = TrnTypeOld.EXPENSE, - period = TimePeriod.currentMonth( - startDayOfMonth = 1 - ), //preview - baseCurrency = "BGN", - totalAmount = 1828.0, - categoryAmounts = listOf( - CategoryAmount( - category = CategoryOld("Bills", Green.toArgb(), icon = "bills"), - amount = 791.0 - ), - CategoryAmount( - category = null, - amount = 497.0, - isCategoryUnspecified = true - ), - CategoryAmount( - category = CategoryOld("Shisha", Orange.toArgb(), icon = "trees"), - amount = 411.93 - ), - CategoryAmount( - category = CategoryOld("Food & Drink", IvyDark.toArgb()), - amount = 260.03 - ), - CategoryAmount( - category = CategoryOld("Gifts", RedLight.toArgb()), - amount = 160.0 - ), - CategoryAmount( - category = CategoryOld("Clothes & Jewelery Fancy", Red.toArgb()), - amount = 2.0 - ), - CategoryAmount( - category = CategoryOld( - "Finances, Burocracy & Governance", - IvyLight.toArgb(), - icon = "work" - ), - amount = 2.0 - ), - ), - selectedCategory = null - ) - - UI(state = state) - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview_Income() { - IvyPreview { - val state = PieChartStatisticState( - transactionType = TrnTypeOld.INCOME, - period = TimePeriod.currentMonth( - startDayOfMonth = 1 - ), //preview - baseCurrency = "BGN", - totalAmount = 1828.0, - categoryAmounts = listOf( - CategoryAmount( - category = CategoryOld("Bills", Green.toArgb(), icon = "bills"), - amount = 791.0 - ), - CategoryAmount( - category = null, - amount = 497.0, - isCategoryUnspecified = true - ), - CategoryAmount( - category = CategoryOld("Shisha", Orange.toArgb(), icon = "trees"), - amount = 411.93 - ), - CategoryAmount( - category = CategoryOld("Food & Drink", IvyDark.toArgb()), - amount = 260.03 - ), - CategoryAmount( - category = CategoryOld("Gifts", RedLight.toArgb()), - amount = 160.0 - ), - CategoryAmount( - category = CategoryOld("Clothes & Jewelery Fancy", Red.toArgb()), - amount = 2.0 - ), - CategoryAmount( - category = CategoryOld( - "Finances, Burocracy & Governance", - IvyLight.toArgb(), - icon = "work" - ), - amount = 2.0 - ), - ), - selectedCategory = null - ) - - UI(state = state) - } -} - diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticState.kt b/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticState.kt deleted file mode 100644 index ae4bb9b03b..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.ivy.pie_charts - -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.pie_charts.model.CategoryAmount -import com.ivy.wallet.ui.theme.modal.ChoosePeriodModalData -import java.util.* - -data class PieChartStatisticState( - val transactionType: TrnTypeOld = TrnTypeOld.INCOME, - val period: TimePeriod = TimePeriod(), - val baseCurrency: String = "", - val totalAmount: Double = 0.0, - val categoryAmounts: List = emptyList(), - val pieChartCategoryAmount: List = emptyList(), - val selectedCategory: SelectedCategory? = null, - val accountIdFilterList: List = emptyList(), - val showCloseButtonOnly: Boolean = false, - val filterExcluded: Boolean = false, - val transactions: List = emptyList(), - val choosePeriodModal: ChoosePeriodModalData? = null, - val showUnpackOption: Boolean = false, - val unpackAllSubCategories: Boolean = false -) \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticViewModel.kt b/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticViewModel.kt deleted file mode 100644 index 68e16d0cd6..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/PieChartStatisticViewModel.kt +++ /dev/null @@ -1,457 +0,0 @@ -package com.ivy.pie_charts - -import com.ivy.base.FromToTimeRange -import com.ivy.base.isSubCategory -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.CategoryOld -import com.ivy.frp.then -import com.ivy.frp.thenInvokeAfter -import com.ivy.frp.viewmodel.FRPViewModel -import com.ivy.pie_charts.action.PieChartAct -import com.ivy.pie_charts.action.SubCategoryAct -import com.ivy.pie_charts.model.CategoryAmount -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.SettingsDao -import com.ivy.wallet.ui.theme.modal.ChoosePeriodModalData -import com.ivy.wallet.utils.dateNowUTC -import com.ivy.wallet.utils.ioThread -import com.ivy.wallet.utils.readOnly -import com.ivy.wallet.utils.replace -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.withContext -import javax.inject.Inject - -@HiltViewModel -class PieChartStatisticViewModel @Inject constructor( - private val settingsDao: SettingsDao, - private val ivyContext: IvyWalletCtx, - private val pieChartAct: PieChartAct, - private val subCategoryAct: SubCategoryAct, - private val sharedPrefs: SharedPrefs -) : FRPViewModel() { - - override val _state: MutableStateFlow = MutableStateFlow( - PieChartStatisticState() - ) - - private val _treatTransfersAsIncomeExpense = MutableStateFlow(false) - private val treatTransfersAsIncomeExpense = _treatTransfersAsIncomeExpense.readOnly() - - override suspend fun handleEvent(event: PieChartStatisticEvent): suspend () -> PieChartStatisticState = - withContext(Dispatchers.Default) { - when (event) { - is PieChartStatisticEvent.Start -> startNew() - is PieChartStatisticEvent.OnSelectNextMonth -> nextMonthNew() - is PieChartStatisticEvent.OnSelectPreviousMonth -> previousMonthNew() - is PieChartStatisticEvent.OnSetPeriod -> onSetPeriodNew(event.timePeriod) - is PieChartStatisticEvent.OnShowMonthModal -> configureMonthModalNew(event.timePeriod) - is PieChartStatisticEvent.OnCategoryClicked -> onCategoryClickedNew(event.category) - is PieChartStatisticEvent.OnSubCategoryListExpanded -> onSubcategoryListExpandNew( - event.parentCategoryAmount, - event.expandedState - ) - is PieChartStatisticEvent.OnUnpackSubCategories -> onSubcategoriesUnpackedNew(event.unpackAllSubCategories) - } - } - - private suspend fun startNew() = suspend { - updateGlobalVariables() - } then { - initializePreliminaryData(ivyContext) - } then { - loadNew(ivyContext.selectedPeriod) - } - - private fun updateGlobalVariables() { -// _treatTransfersAsIncomeExpense.value = screen.treatTransfersAsIncomeExpense - } - - private suspend fun initializePreliminaryData( - ivyContext: IvyWalletCtx - ) = suspend { - val baseCurrency = ioThread { settingsDao.findFirstSuspend() }.currency - baseCurrency - } thenInvokeAfter { baseCurrency -> -// updateState { -// it.copy( -// period = ivyContext.selectedPeriod, -// transactionType = screen.type, -// accountIdFilterList = screen.accountList, -// filterExcluded = screen.filterExcluded, -// transactions = screen.transactions, -// showCloseButtonOnly = screen.transactions.isNotEmpty(), -// baseCurrency = baseCurrency, -// unpackAllSubCategories = false, -// showUnpackOption = false -// ) -// } - } - - //----------------------------------------------------------------------------------- - private suspend fun loadNew(timePeriod: TimePeriod) = - suspend { timePeriod } then - ::computePieChartActInputParams then - ::computePieChartAct then - ::applySubCategoriesTransformation then - ::computeChartSpecificUIList thenInvokeAfter { - finishLoading(timePeriod, input = it) - } - - private fun computePieChartActInputParams( - period: TimePeriod, - state: PieChartStatisticState = stateVal() - ): Pair { - val treatTransferAsIncExp = - sharedPrefs.getBoolean( - SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, - false - ) && state.accountIdFilterList.isNotEmpty() && treatTransfersAsIncomeExpense.value - - val range = period.toRange(ivyContext.startDayOfMonth) - - return Pair(treatTransferAsIncExp, range) - } - - private suspend fun computePieChartAct( - input: Pair, - state: PieChartStatisticState = stateVal() - ): PieChartAct.Output { - val (treatTransferAsIncExp, timePeriodRange) = input - - return ioThread { - pieChartAct( - PieChartAct.Input( - baseCurrency = state.baseCurrency, - range = timePeriodRange, - type = state.transactionType, - accountIdFilterList = state.accountIdFilterList, - treatTransferAsIncExp = treatTransferAsIncExp, - existingTransactions = state.transactions, - showAccountTransfersCategory = state.accountIdFilterList.isNotEmpty(), - filterEmptyCategoryAmounts = false - ) - ) - } - } - - private suspend fun applySubCategoriesTransformation(input: PieChartAct.Output) = - suspend { input.categoryAmounts } then subCategoryAct thenInvokeAfter { list -> - input.copy(categoryAmounts = list) - } - - private fun computeChartSpecificUIList( - input: PieChartAct.Output - ): Pair, PieChartAct.Output> { - val chartUISpecificList = - input.categoryAmounts.map { cat -> cat.copy(amount = cat.totalAmount()) } - - return Pair(chartUISpecificList, input) - } - - private suspend fun finishLoading( - timePeriod: TimePeriod, - input: Pair, PieChartAct.Output> - ) = suspend { - computeShowUnpackAllCategoriesOption( - chartUISpecificList = input.first, - categoryAmountList = input.second.categoryAmounts - ) - } thenInvokeAfter { showUnpackAllCategoriesOption -> - - val (chartUISpecificList, output) = input - updateState { - it.copy( - period = timePeriod, - totalAmount = output.totalAmount, - categoryAmounts = output.categoryAmounts, - pieChartCategoryAmount = chartUISpecificList, - selectedCategory = null, - showUnpackOption = showUnpackAllCategoriesOption - ) - } - } - - private fun computeShowUnpackAllCategoriesOption( - chartUISpecificList: List, - categoryAmountList: List - ) = chartUISpecificList.size != - categoryAmountList.filter { c -> c.subCategoryState.subCategoriesList.isEmpty() }.size - - //----------------------------------------------------------------------------------- - private suspend fun onSetPeriodNew(period: TimePeriod) = suspend { - ivyContext.updateSelectedPeriodInMemory(period) - } then { period } then ::loadNew - - //----------------------------------------------------------------------------------- - private suspend fun nextMonthNew() = suspend { - val month = stateVal().period.month - val year = stateVal().period.year ?: dateNowUTC().year - - Pair(month, year) - } then { - val (month, year) = it - - if (month != null) { - loadNew(month.incrementMonthPeriod(ivyContext, 1L, year)) - } else - stateVal() - } - - //----------------------------------------------------------------------------------- - private suspend fun previousMonthNew() = suspend { - val month = stateVal().period.month - val year = stateVal().period.year ?: dateNowUTC().year - - Pair(month, year) - } then { - val (month, year) = it - - if (month != null) { - loadNew(month.incrementMonthPeriod(ivyContext, -1L, year)) - } else - stateVal() - } - - //----------------------------------------------------------------------------------- - private suspend fun configureMonthModalNew(timePeriod: TimePeriod?) = suspend { - val choosePeriodModalData = timePeriod?.let { ChoosePeriodModalData(period = it) } - choosePeriodModalData - } then { choosePeriodModalData -> - updateState { - it.copy(choosePeriodModal = choosePeriodModalData) - } - } - - //----------------------------------------------------------------------------------- - private suspend fun onCategoryClickedNew(clickedCategory: CategoryOld?) = - suspend { clickedCategory } then - ::computeReorderParams then - ::reorderSelectedCategoryToTop then - ::finishReordering - - private fun computeReorderParams(clickedCategory: CategoryOld?): Pair { - val selectedCategory = if (clickedCategory == stateVal().selectedCategory?.category) - null - else - SelectedCategory(category = clickedCategory) - - val categoryToSort = - if (selectedCategory?.category != null && selectedCategory.category.isSubCategory() - && !isSubCategoryListUnpacked() - ) { - SelectedCategory(findParentCategory(selectedCategory.category)) - } else { - selectedCategory - } - - return Pair(selectedCategory, categoryToSort) - } - - private fun reorderSelectedCategoryToTop( - input: Pair - ): Pair> { - val (selectedCategory, categoryToSort) = input - val existingCategoryAmounts = stateVal().categoryAmounts - val newCategoryAmounts = if (categoryToSort != null && selectedCategory != null) { - existingCategoryAmounts - .sortedByDescending { it.totalAmount() } - .sortedByDescending { categoryToSort.category == it.category } - .map { parentCategory -> - val subCatList = parentCategory.subCategoryState.subCategoriesList - .sortedByDescending { sc -> sc.amount } - .sortedByDescending { sc -> selectedCategory.category == sc.category } - - parentCategory.copy( - subCategoryState = parentCategory.subCategoryState.copy( - subCategoriesList = subCatList - ) - ) - } - } else { - existingCategoryAmounts.sortedByDescending { - it.totalAmount() - } - } - - return Pair(selectedCategory, newCategoryAmounts) - } - - private suspend fun finishReordering(input: Pair>) = - suspend { - updateState { - it.copy( - selectedCategory = input.first, - categoryAmounts = input.second - ) - } - } thenInvokeAfter { state -> - state - } - - //----------------------------------------------------------------------------------- - private suspend fun onSubcategoryListExpandNew( - parentCategoryAmt: CategoryAmount, - expandedState: Boolean - ) = suspend { - computeSubcategoryListParams(parentCategoryAmt, expandedState) - } then ::finishOnSubcategoryListExpand - - private suspend fun computeSubcategoryListParams( - parentCategoryAmt: CategoryAmount, - expandedState: Boolean, - state: PieChartStatisticState = stateVal() - ) = suspend { - //Update CategoryExpansion State - val newCategoryAmount = parentCategoryAmt.copy( - subCategoryState = parentCategoryAmt.subCategoryState.copy(subCategoryListExpanded = expandedState) - ) - state.categoryAmounts.replace(parentCategoryAmt, newCategoryAmount) - } then { newCategoryAmountList -> - val updatedChartSpecificUIList = computeUpdatedPieChartList( - parentCategoryAmt = parentCategoryAmt, - pieChartCategoryAmountList = state.pieChartCategoryAmount, - expandedState = expandedState - ) - Pair(newCategoryAmountList, updatedChartSpecificUIList) - } thenInvokeAfter { - /** - * Returns parentCategory if a subcategory is selected, - * Returns parentCategory if a parent category is Selected, - * Returns null on subsequent selection of same category - */ - val newSelectedCategory = computeUpdatedSelectedCategory( - state.selectedCategory, - parentCategoryAmt.category - ) - - Triple(newSelectedCategory, it.first, it.second) - } - - private suspend fun computeUpdatedPieChartList( - parentCategoryAmt: CategoryAmount, - pieChartCategoryAmountList: List, - expandedState: Boolean - ): List { - - /** - * For a given parentCategory @param [parentCatAmt] - * returns a list where all the subcategories are present directly under the parent category - */ - fun flattenPieChartListWithOrderPreserved( - parentCatAmt: CategoryAmount, - pieChartCatAmount: List - ): List { - val newPieChartList = mutableListOf() - pieChartCatAmount.forEach { c -> - newPieChartList.add(c) - if (c.category?.id == parentCatAmt.category?.id) { - parentCatAmt.subCategoryState.subCategoriesList.forEach { sc -> - newPieChartList.add(sc) - } - } - } - return newPieChartList - } - - /** - * Replace the original object with [replacementCatAmt] parameter, - * using replacementCatAmt.category.id as the key - */ - fun replaceCategoryAmount( - replacementCatAmt: CategoryAmount, - pieChartCatAmt: List = pieChartCategoryAmountList - ): List { - return pieChartCatAmt.replace( - { c -> - c.category?.id == replacementCatAmt.category?.id - }, - replacementCatAmt - ) - } - - - return if (expandedState) - suspend { replaceCategoryAmount(parentCategoryAmt) } thenInvokeAfter { list -> - flattenPieChartListWithOrderPreserved(parentCategoryAmt, list) - } - else - suspend { - replaceCategoryAmount( - parentCategoryAmt.copy(amount = parentCategoryAmt.totalAmount()) - ) - } then { - it.minus(parentCategoryAmt.subCategoryState.subCategoriesList.toSet()) - } thenInvokeAfter { - it.sortedByDescending { ca -> ca.amount } - } - } - - //clears the selectedCategory if and only if selected category is subcategory of parentCategory - private fun computeUpdatedSelectedCategory( - selectedCategory: SelectedCategory?, - parentCategory: CategoryOld? - ): SelectedCategory? { - return if (selectedCategory != null && selectedCategory.category?.parentCategoryId == parentCategory?.id) - null - else - selectedCategory - } - - private suspend fun finishOnSubcategoryListExpand(it: Triple, List>): PieChartStatisticState { - val (selectedCategory, categoryAmounts, chartSpecificUIList) = it - - return updateState { - it.copy( - pieChartCategoryAmount = chartSpecificUIList, - categoryAmounts = categoryAmounts, - selectedCategory = selectedCategory - ) - } - } - - //----------------------------------------------------------------------------------- - private suspend fun onSubcategoriesUnpackedNew(unpackAllSubCategories: Boolean) = suspend { - stateVal().categoryAmounts - } then { - packUnpackCategoryList(unpackAllSubCategories, it) - } then { newCategoryList -> - updateState { - it.copy( - pieChartCategoryAmount = newCategoryList.map { cat -> cat.copy(amount = cat.totalAmount()) }, - categoryAmounts = newCategoryList, - unpackAllSubCategories = unpackAllSubCategories, - selectedCategory = null - ) - } - } - - private suspend fun packUnpackCategoryList( - unpackAllSubCategories: Boolean, - categoryAmountList: List - ): List { - return if (unpackAllSubCategories) - categoryAmountList.flatMap { - val list = mutableListOf() - list.addAll(it.subCategoryState.subCategoriesList) - list.add(it.clearSubcategoriesAndGet()) - - list - }.sortedByDescending { - it.totalAmount() - } - else - suspend { stateVal().categoryAmounts } thenInvokeAfter { - subCategoryAct(it) - } - } - - private fun findParentCategory(subCategory: CategoryOld): CategoryOld? { - return stateVal().categoryAmounts.find { it.category?.id == subCategory.parentCategoryId }?.category - } - - private fun isSubCategoryListUnpacked() = - stateVal().categoryAmounts.size == stateVal().pieChartCategoryAmount.size -} \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/SelectedCategory.kt b/pie-charts/src/main/java/com/ivy/pie_charts/SelectedCategory.kt deleted file mode 100644 index 3e067e1a8b..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/SelectedCategory.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.pie_charts - -import com.ivy.data.CategoryOld - -data class SelectedCategory( - val category: CategoryOld? //null - Unspecified -) \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/action/PieChartAct.kt b/pie-charts/src/main/java/com/ivy/pie_charts/action/PieChartAct.kt deleted file mode 100644 index 529457e9ed..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/action/PieChartAct.kt +++ /dev/null @@ -1,290 +0,0 @@ -//package com.ivy.pie_charts.action -// -//import androidx.compose.ui.graphics.toArgb -//import com.ivy.base.FromToTimeRange -//import com.ivy.base.R -//import com.ivy.data.AccountOld -//import com.ivy.data.CategoryOld -//import com.ivy.data.transaction.TransactionOld -//import com.ivy.data.transaction.TrnTypeOld -//import com.ivy.design.l0_system.color.RedLight -// -// -//import com.ivy.frp.action.FPAction -//import com.ivy.frp.action.thenFilter -//import com.ivy.frp.action.thenMap -//import com.ivy.frp.then -//import com.ivy.pie_charts.model.CategoryAmount -//import com.ivy.wallet.domain.action.account.AccountsActOld -//import com.ivy.wallet.domain.action.category.CategoriesActOld -//import com.ivy.wallet.domain.action.category.CategoryIncomeWithAccountFiltersAct -//import com.ivy.wallet.domain.action.transaction.CalcTrnsIncomeExpenseAct -//import com.ivy.wallet.domain.action.transaction.TrnsWithRangeAndAccFiltersAct -//import com.ivy.wallet.domain.pure.account.filterExcluded -//import com.ivy.wallet.domain.pure.data.IncomeExpenseTransferPair -//import java.math.BigDecimal -//import java.util.* -//import javax.inject.Inject -// -//class PieChartAct @Inject constructor( -// private val accountsAct: AccountsActOld, -// private val trnsWithRangeAndAccFiltersAct: TrnsWithRangeAndAccFiltersAct, -// private val calcTrnsIncomeExpenseAct: CalcTrnsIncomeExpenseAct, -// private val categoriesAct: CategoriesActOld, -// private val categoryIncomeWithAccountFiltersAct: CategoryIncomeWithAccountFiltersAct -//) : FPAction() { -// -// private val accountTransfersCategory = -// CategoryOld( -// com.ivy.core.ui.temp.stringRes(R.string.account_transfers), -// RedLight.toArgb(), -// "transfer" -// ) -// -// override suspend fun Input.compose(): suspend () -> Output = suspend { -// getUsableAccounts( -// accountIdFilterList = accountIdFilterList, -// allAccounts = suspend { accountsAct(Unit) } -// ) -// } then { -// val accountsUsed = it.first -// val accountIdFilterSet = it.second -// -// val transactions = existingTransactions.ifEmpty { -// trnsWithRangeAndAccFiltersAct( -// TrnsWithRangeAndAccFiltersAct.Input( -// range = range, -// accountIdFilterSet = accountIdFilterSet -// ) -// ) -// } -// -// Pair(accountsUsed, transactions) -// } then { -// val accountsUsed = it.first -// val transactions = it.second -// -// val incomeExpenseTransfer = calcTrnsIncomeExpenseAct( -// CalcTrnsIncomeExpenseAct.Input( -// transactions = transactions, -// accounts = accountsUsed, -// baseCurrency = baseCurrency -// ) -// ) -// -// val categoryAmounts = suspend { -// calculateCategoryAmounts( -// type = type, -// baseCurrency = baseCurrency, -// allCategories = suspend { -// categoriesAct(Unit).plus(null) //for unspecified -// }, -// transactions = suspend { transactions }, -// accountsUsed = suspend { accountsUsed }, -// addAssociatedTransToCategoryAmt = existingTransactions.isNotEmpty(), -// filterEmptyCategoryAmounts = filterEmptyCategoryAmounts -// ) -// } then { -// addAccountTransfersCategory( -// showAccountTransfersCategory = showAccountTransfersCategory, -// type = type, -// accountTransfersCategory = accountTransfersCategory, -// accountIdFilterSet = accountIdFilterList.toHashSet(), -// incomeExpenseTransfer = suspend { incomeExpenseTransfer }, -// categoryAmounts = suspend { it }, -// transactions = suspend { transactions } -// ) -// } -// -// Pair(incomeExpenseTransfer, categoryAmounts()) -// } then { -// -// val totalAmount = calculateTotalAmount( -// type = type, -// treatTransferAsIncExp = treatTransferAsIncExp, -// incomeExpenseTransfer = suspend { it.first } -// ) -// -// val catAmountList = it.second -// -// Pair(totalAmount, catAmountList) -// } then { -// Output(it.first.toDouble(), it.second) -// } -// -// -// private suspend fun getUsableAccounts( -// accountIdFilterList: List, -// -// -// allAccounts: suspend () -> List -// ): Pair, Set> { -// -// val accountsUsed = if (accountIdFilterList.isEmpty()) -// allAccounts then ::filterExcluded -// else -// allAccounts thenFilter { -// accountIdFilterList.contains(it.id) -// } -// -// val accountsUsedIDSet = accountsUsed thenMap { it.id } then { it.toHashSet() } -// -// return Pair(accountsUsed(), accountsUsedIDSet()) -// } -// -// -// private suspend fun calculateCategoryAmounts( -// type: TrnTypeOld, -// baseCurrency: String, -// addAssociatedTransToCategoryAmt: Boolean = false, -// filterEmptyCategoryAmounts: Boolean = true, -// -// -// allCategories: suspend () -> List, -// -// -// transactions: suspend () -> List, -// -// -// accountsUsed: suspend () -> List, -// ): List { -// val trans = transactions() -// val accUsed = accountsUsed() -// -// val catAmtList = allCategories thenMap { category -> -// val categoryTransactions = asyncIo { -// if (addAssociatedTransToCategoryAmt) -// trans.filter { -// it.type == type && it.categoryId == category?.id -// } -// else -// emptyList() -// } -// -// val catIncomeExpense = categoryIncomeWithAccountFiltersAct( -// CategoryIncomeWithAccountFiltersAct.Input( -// transactions = trans, -// accountFilterList = accUsed, -// category = category, -// baseCurrency = baseCurrency -// ) -// ) -// -// CategoryAmount( -// category = category, -// amount = when (type) { -// TrnTypeOld.INCOME -> catIncomeExpense.income.toDouble() -// TrnTypeOld.EXPENSE -> catIncomeExpense.expense.toDouble() -// else -> error("not supported transactionType - $type") -// }, -// associatedTransactions = categoryTransactions.await(), -// isCategoryUnspecified = category == null -// ) -// } thenFilter { catAmt -> -// if (filterEmptyCategoryAmounts) -// catAmt.amount != 0.0 -// else -// true -// } then { -// it.sortedByDescending { ca -> ca.amount } -// } -// -// return catAmtList() -// } -// -// -// private suspend fun calculateTotalAmount( -// type: TrnTypeOld, -// treatTransferAsIncExp: Boolean, -// -// -// incomeExpenseTransfer: suspend () -> IncomeExpenseTransferPair -// ): BigDecimal { -// val incExpQuad = incomeExpenseTransfer() -// return when (type) { -// TrnTypeOld.INCOME -> { -// incExpQuad.income + -// if (treatTransferAsIncExp) -// incExpQuad.transferIncome -// else -// BigDecimal.ZERO -// } -// TrnTypeOld.EXPENSE -> { -// incExpQuad.expense + -// if (treatTransferAsIncExp) -// incExpQuad.transferExpense -// else -// BigDecimal.ZERO -// } -// else -> BigDecimal.ZERO -// } -// } -// -// -// private suspend fun addAccountTransfersCategory( -// showAccountTransfersCategory: Boolean, -// type: TrnTypeOld, -// accountTransfersCategory: CategoryOld, -// accountIdFilterSet: Set, -// -// -// transactions: suspend () -> List, -// -// -// incomeExpenseTransfer: suspend () -> IncomeExpenseTransferPair, -// -// -// categoryAmounts: suspend () -> List -// ): List { -// -// val incExpQuad = incomeExpenseTransfer() -// -// val catAmtList = -// if (!showAccountTransfersCategory || incExpQuad.transferIncome == BigDecimal.ZERO && incExpQuad.transferExpense == BigDecimal.ZERO) -// categoryAmounts then { it.sortedByDescending { ca -> ca.amount } } -// else { -// -// val amt = if (type == TrnTypeOld.INCOME) -// incExpQuad.transferIncome.toDouble() -// else -// incExpQuad.transferExpense.toDouble() -// -// val categoryTrans = transactions().filter { -// it.type == TrnTypeOld.TRANSFER && it.categoryId == null -// }.filter { -// if (type == TrnTypeOld.EXPENSE) -// accountIdFilterSet.contains(it.accountId) -// else -// accountIdFilterSet.contains(it.toAccountId) -// } -// -// categoryAmounts then { -// it.plus( -// CategoryAmount( -// category = accountTransfersCategory, -// amount = amt, -// associatedTransactions = categoryTrans, -// isCategoryUnspecified = true -// ) -// ) -// } then { -// it.sortedByDescending { ca -> ca.amount } -// } -// } -// -// return catAmtList() -// } -// -// data class Input( -// val baseCurrency: String, -// val range: FromToTimeRange, -// val type: TrnTypeOld, -// val accountIdFilterList: List, -// val treatTransferAsIncExp: Boolean = false, -// val showAccountTransfersCategory: Boolean = treatTransferAsIncExp, -// val existingTransactions: List = emptyList(), -// val filterEmptyCategoryAmounts: Boolean = true -// ) -// -// data class Output(val totalAmount: Double, val categoryAmounts: List) -//} \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/action/SubCategoryAct.kt b/pie-charts/src/main/java/com/ivy/pie_charts/action/SubCategoryAct.kt deleted file mode 100644 index f1120f01a1..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/action/SubCategoryAct.kt +++ /dev/null @@ -1,66 +0,0 @@ -//package com.ivy.pie_charts.action -// -//import com.ivy.pie_charts.model.CategoryAmount -//import com.ivy.frp.action.FPAction -//import com.ivy.frp.then -//import com.ivy.frp.thenInvokeAfter -//import java.util.* -//import javax.inject.Inject -// -//class SubCategoryAct @Inject constructor() : -// FPAction, List>() { -// -// override suspend fun List.compose(): suspend () -> List = -// suspend { this } then -// ::groupByParentCategoryID then -// ::mapParentCategoryIdToCategoryAmount then -// ::flattenToList -// -// private fun groupByParentCategoryID( -// categoryAmounts: List -// ): Pair, Map>> { -// val groupedMap = categoryAmounts.groupBy { -// it.category?.parentCategoryId ?: it.category?.id -// } -// -// return Pair(categoryAmounts, groupedMap) -// } -// -// private fun mapParentCategoryIdToCategoryAmount( -// pair: Pair, Map>> -// ): Map> { -// val categoriesSet = pair.first.toHashSet() -// val groupedMap = pair.second -// -// return groupedMap.mapKeys { mapEntry -> -// categoriesSet.find { cat -> mapEntry.key == cat.category?.id } ?: CategoryAmount( -// null, -// 0.0 -// ) -// } -// } -// -// private suspend fun flattenToList( -// groupedMap: Map> -// ): List = suspend { -// groupedMap.map { mapEntry -> -// val parentCategory = mapEntry.key -// val subCatList = mapEntry.value.filter { subCategory -> subCategory != parentCategory } -// .filter { sc -> sc.totalAmount() != 0.0 } -// val subCatTotalAmount = subCatList.sumOf { subCategory -> subCategory.amount } -// -// parentCategory.copy( -// amount = parentCategory.amount, -// subCategoryState = CategoryAmount.SubCategoryState( -// subCategoriesList = subCatList, -// subCategoryTotalAmount = subCatTotalAmount, -// subCategoryListExpanded = false -// ) -// ) -// } -// } then { -// it.sortedByDescending { categoryAmount -> categoryAmount.totalAmount() } -// } thenInvokeAfter { -// it.filter { categoryAmount -> categoryAmount.totalAmount() != 0.0 } -// } -//} \ No newline at end of file diff --git a/pie-charts/src/main/java/com/ivy/pie_charts/model/CategoryAmount.kt b/pie-charts/src/main/java/com/ivy/pie_charts/model/CategoryAmount.kt deleted file mode 100644 index a0a18681c6..0000000000 --- a/pie-charts/src/main/java/com/ivy/pie_charts/model/CategoryAmount.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.ivy.pie_charts.model - -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld - -data class CategoryAmount( - val category: CategoryOld?, - val amount: Double, - val associatedTransactions: List = emptyList(), - val isCategoryUnspecified: Boolean = false, - val subCategoryState: SubCategoryState = SubCategoryState(), -) { - fun totalAmount(): Double = amount + subCategoryState.subCategoryTotalAmount - fun clearSubcategoriesAndGet(): CategoryAmount { - return this.copy(subCategoryState = SubCategoryState()) - } - - fun getRelevantAmount() = if (subCategoryState.subCategoryListExpanded) - amount - else - totalAmount() - - data class SubCategoryState( - val subCategoriesList: List = emptyList(), - val subCategoryTotalAmount: Double = 0.0, - val subCategoryListExpanded: Boolean = false, - ) -} \ No newline at end of file diff --git a/planned-payments/.gitignore b/planned-payments/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/planned-payments/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/planned-payments/README.md b/planned-payments/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/planned-payments/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/planned-payments/build.gradle.kts b/planned-payments/build.gradle.kts deleted file mode 100644 index f8e1b2d53a..0000000000 --- a/planned-payments/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -import com.ivy.buildsrc.EventBus -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) - EventBus() -} \ No newline at end of file diff --git a/planned-payments/src/main/AndroidManifest.xml b/planned-payments/src/main/AndroidManifest.xml deleted file mode 100644 index 029d9f5f92..0000000000 --- a/planned-payments/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedScreen.kt b/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedScreen.kt deleted file mode 100644 index 0a131e93dc..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedScreen.kt +++ /dev/null @@ -1,444 +0,0 @@ -package com.ivy.wallet.ui.planned.edit - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.planned.IntervalType -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.ui.edit.core.* -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.ui.theme.components.ChangeTransactionTypeModal -import com.ivy.wallet.ui.theme.modal.DeleteModal -import com.ivy.wallet.ui.theme.modal.ModalSet -import com.ivy.wallet.ui.theme.modal.RecurringRuleModal -import com.ivy.wallet.ui.theme.modal.RecurringRuleModalData -import com.ivy.wallet.ui.theme.modal.edit.* -import java.time.LocalDateTime -import java.util.* - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.EditPlannedScreen() { - val viewModel: EditPlannedViewModel = hiltViewModel() - - val startDate by viewModel.startDate.observeAsState() - val intervalN by viewModel.intervalN.observeAsState() - val intervalType by viewModel.intervalType.observeAsState() - val oneTime by viewModel.oneTime.observeAsState(false) - - val transactionType by viewModel.transactionType.observeAsState() - val initialTitle by viewModel.initialTitle.observeAsState() - val currency by viewModel.currency.observeAsState("") - val description by viewModel.description.observeAsState() - val category by viewModel.category.observeAsState() - val account by viewModel.account.observeAsState() - val amount by viewModel.amount.observeAsState(0.0) - - val categories by viewModel.categories.observeAsState(emptyList()) - val accounts by viewModel.accounts.observeAsState(emptyList()) - - UI( -// screen = screen, - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime, - type = TrnTypeOld.TRANSFER, - currency = currency, - initialTitle = initialTitle, - description = description, - category = category, - account = account, - amount = amount, - - categories = categories, - accounts = accounts, - - onRuleChanged = viewModel::onRuleChanged, - onTitleChanged = viewModel::onTitleChanged, - onDescriptionChanged = viewModel::onDescriptionChanged, - onAmountChanged = viewModel::onAmountChanged, - onCategoryChanged = viewModel::onCategoryChanged, - onAccountChanged = viewModel::onAccountChanged, - onSetTransactionType = viewModel::onSetTransactionType, - - onCreateCategory = viewModel::createCategory, - onSave = viewModel::save, - onDelete = viewModel::delete, - onCreateAccount = viewModel::createAccount - ) -} - -/** - * Flow Empty: Type -> Amount -> Category -> Recurring Rule -> Title - * Flow Amount + Category: Recurring Rule -> Title - */ - -@ExperimentalFoundationApi -@Composable -private fun BoxWithConstraintsScope.UI( -// screen: EditPlanned, - - startDate: LocalDateTime?, - intervalN: Int?, - intervalType: IntervalType?, - oneTime: Boolean, - - type: TrnTypeOld, - currency: String, - initialTitle: String?, - description: String?, - category: CategoryOld?, - account: AccountOld?, - amount: Double, - - categories: List, - accounts: List, - - onRuleChanged: (LocalDateTime, oneTime: Boolean, Int?, IntervalType?) -> Unit, - onTitleChanged: (String?) -> Unit, - onDescriptionChanged: (String?) -> Unit, - onAmountChanged: (Double) -> Unit, - onCategoryChanged: (CategoryOld?) -> Unit, - onAccountChanged: (AccountOld) -> Unit, - onSetTransactionType: (TrnTypeOld) -> Unit, - - onCreateCategory: (CreateCategoryData) -> Unit = {}, - onSave: () -> Unit, - onDelete: () -> Unit, - onCreateAccount: (CreateAccountData) -> Unit = {}, -) { - var chooseCategoryModalVisible by remember { mutableStateOf(false) } - var categoryModalData: CategoryModalData? by remember { mutableStateOf(null) } - var accountModalData: AccountModalData? by remember { mutableStateOf(null) } - var descriptionModalVisible by remember { mutableStateOf(false) } - var deleteTrnModalVisible by remember { mutableStateOf(false) } - var changeTransactionTypeModalVisible by remember { mutableStateOf(false) } - var amountModalShown by remember { mutableStateOf(false) } - var recurringRuleModal: RecurringRuleModalData? by remember { mutableStateOf(null) } - - var titleTextFieldValue by remember(initialTitle) { - mutableStateOf( - TextFieldValue( - initialTitle ?: "" - ) - ) - } - val titleFocus = FocusRequester() - - Column( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - .verticalScroll(rememberScrollState()) - ) { - Spacer(Modifier.height(16.dp)) - - Toolbar( - type = type, - initialTransactionId = UUID.randomUUID(), - onDeleteTrnModal = { - deleteTrnModalVisible = true - }, - onChangeTransactionTypeModal = { - changeTransactionTypeModalVisible = true - } - ) - - Spacer(Modifier.height(32.dp)) - - Title( - type = type, - titleFocus = titleFocus, - initialTransactionId = UUID.randomUUID(), - - titleTextFieldValue = titleTextFieldValue, - setTitleTextFieldValue = { - titleTextFieldValue = it - }, - suggestions = emptySet(), //DO NOT display title suggestions for "Planned Payments" - - onTitleChanged = onTitleChanged, - onNext = { - when { - shouldFocusRecurring(startDate, intervalN, intervalType, oneTime) -> { - recurringRuleModal = RecurringRuleModalData( - initialStartDate = startDate, - initialIntervalN = intervalN, - initialIntervalType = intervalType, - initialOneTime = oneTime - ) - } - else -> { - onSave() - } - } - } - ) - - if (type != TrnTypeOld.TRANSFER) { - Spacer(Modifier.height(32.dp)) - - Category( - category = category, - onChooseCategory = { - chooseCategoryModalVisible = true - } - ) - } - - Spacer(Modifier.height(32.dp)) - - RecurringRule( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime, - onShowRecurringRuleModal = { - recurringRuleModal = RecurringRuleModalData( - initialStartDate = startDate, - initialIntervalN = intervalN, - initialIntervalType = intervalType, - initialOneTime = oneTime - ) - } - ) - - Spacer(Modifier.height(12.dp)) - - Description( - description = description, - onAddDescription = { descriptionModalVisible = true }, - onEditDescription = { descriptionModalVisible = true } - ) - - Spacer(Modifier.height(600.dp))//scroll hack - } - - EditBottomSheet( - initialTransactionId = UUID.randomUUID(), - type = type, - accounts = accounts, - selectedAccount = account, - toAccount = null, - amount = amount, - currency = currency, - - ActionButton = { - ModalSet( - modifier = Modifier.testTag("editPlannedScreen_set") - ) { - onSave() - } - }, - - amountModalShown = amountModalShown, - setAmountModalShown = { - amountModalShown = it - }, - - onAmountChanged = { - onAmountChanged(it) - when { - shouldFocusCategory(category, type) -> { - chooseCategoryModalVisible = true - } - shouldFocusRecurring(startDate, intervalN, intervalType, oneTime) -> { - recurringRuleModal = RecurringRuleModalData( - initialStartDate = startDate, - initialIntervalN = intervalN, - initialIntervalType = intervalType, - initialOneTime = oneTime - ) - } - shouldFocusTitle(titleTextFieldValue, type) -> { - titleFocus.requestFocus() - } - } - }, - onSelectedAccountChanged = onAccountChanged, - onToAccountChanged = { }, - onAddNewAccount = { - accountModalData = AccountModalData( - account = null, - baseCurrency = currency, - balance = 0.0 - ) - } - ) - - //Modals - ChooseCategoryModal( - visible = chooseCategoryModalVisible, - initialCategory = category, - categories = categories, - showCategoryModal = { categoryModalData = CategoryModalData(it) }, - onCategoryChanged = { - onCategoryChanged(it) - - recurringRuleModal = RecurringRuleModalData( - initialStartDate = startDate, - initialIntervalN = intervalN, - initialIntervalType = intervalType, - initialOneTime = oneTime - ) - }, - dismiss = { - chooseCategoryModalVisible = false - } - ) - - CategoryModal( - modal = categoryModalData, - onCreateCategory = onCreateCategory, - onEditCategory = { }, - dismiss = { - categoryModalData = null - } - ) - - AccountModal( - modal = accountModalData, - onCreateAccount = onCreateAccount, - onEditAccount = { _, _ -> }, - dismiss = { - accountModalData = null - } - ) - - DescriptionModal( - visible = descriptionModalVisible, - description = description, - onDescriptionChanged = onDescriptionChanged, - dismiss = { - descriptionModalVisible = false - } - ) - - DeleteModal( - visible = deleteTrnModalVisible, - title = stringResource(R.string.confirm_deletion), - description = stringResource(R.string.planned_payment_confirm_deletion_description), - dismiss = { deleteTrnModalVisible = false } - ) { - onDelete() - } - - ChangeTransactionTypeModal( - title = stringResource(R.string.set_payment_type), - visible = changeTransactionTypeModalVisible, - includeTransferType = false, - initialType = type, - dismiss = { - changeTransactionTypeModalVisible = false - } - ) { - onSetTransactionType(it) - if (shouldFocusAmount(amount)) { - amountModalShown = true - } - } - - RecurringRuleModal( - modal = recurringRuleModal, - onRuleChanged = { newStartDate, newOneTime, newIntervalN, newIntervalType -> - onRuleChanged(newStartDate, newOneTime, newIntervalN, newIntervalType) - - when { - shouldFocusCategory(category, type) -> { - chooseCategoryModalVisible = true - } - shouldFocusTitle(titleTextFieldValue, type) -> { - titleFocus.requestFocus() - } - } - }, - dismiss = { - recurringRuleModal = null - } - ) -} - -private fun shouldFocusCategory( - category: CategoryOld?, - type: TrnTypeOld -): Boolean = category == null && type != TrnTypeOld.TRANSFER - -private fun shouldFocusTitle( - titleTextFieldValue: TextFieldValue, - type: TrnTypeOld -): Boolean = titleTextFieldValue.text.isBlank() && type != TrnTypeOld.TRANSFER - -private fun shouldFocusRecurring( - startDate: LocalDateTime?, - intervalN: Int?, - intervalType: IntervalType?, - oneTime: Boolean, -): Boolean { - return !hasRecurringRule( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime - ) -} - -private fun shouldFocusAmount(amount: Double) = amount == 0.0 - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview() { - IvyPreview { - UI( -// screen = EditPlanned(null, TrnTypeOld.EXPENSE), - oneTime = false, - startDate = null, - intervalN = null, - intervalType = null, - initialTitle = "", - currency = "BGN", - description = null, - category = null, - account = AccountOld(name = "phyre", color = Green.toArgb()), - amount = 0.0, - type = TrnTypeOld.INCOME, - - categories = emptyList(), - accounts = emptyList(), - - onRuleChanged = { _, _, _, _ -> }, - onCategoryChanged = {}, - onAccountChanged = {}, - onDescriptionChanged = {}, - onTitleChanged = {}, - onAmountChanged = {}, - - onCreateCategory = { }, - onSave = {}, - onDelete = {}, - onCreateAccount = { }, - onSetTransactionType = {} - ) - } -} \ No newline at end of file diff --git a/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt b/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt deleted file mode 100644 index 7a008e628e..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt +++ /dev/null @@ -1,357 +0,0 @@ -package com.ivy.wallet.ui.planned.edit - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.planned.IntervalType -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.frp.test.TestIdlingResource - -import com.ivy.temp.event.AccountsUpdatedEvent -import com.ivy.wallet.domain.action.account.AccountsActOld -import com.ivy.wallet.domain.action.category.CategoriesActOld -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.domain.deprecated.sync.item.TransactionSync -import com.ivy.wallet.domain.deprecated.sync.uploader.PlannedPaymentRuleUploader -import com.ivy.wallet.io.persistence.dao.* -import com.ivy.wallet.io.persistence.data.toEntity -import com.ivy.wallet.utils.asLiveData -import com.ivy.wallet.utils.ioThread -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.greenrobot.eventbus.EventBus -import java.time.LocalDateTime -import javax.inject.Inject - -@HiltViewModel -class EditPlannedViewModel @Inject constructor( - private val transactionDao: TransactionDao, - private val accountDao: AccountDao, - private val categoryDao: CategoryDao, - private val settingsDao: SettingsDao, - private val ivyContext: IvyWalletCtx, - private val - private val transactionSync: TransactionSync, - private val plannedPaymentRuleDao: PlannedPaymentRuleDao, - private val plannedPaymentRuleUploader: PlannedPaymentRuleUploader, - private val plannedPaymentsGenerator: PlannedPaymentsGenerator, - private val categoryCreator: CategoryCreator, - private val accountCreator: AccountCreator, - private val accountsAct: AccountsActOld, - private val categoriesAct: CategoriesActOld -) : ViewModel() { - - private val _transactionType = MutableLiveData() - val transactionType = _transactionType - - private val _startDate = MutableLiveData() - val startDate = _startDate.asLiveData() - - private val _intervalN = MutableLiveData() - val intervalN = _intervalN.asLiveData() - - private val _intervalType = MutableLiveData() - val intervalType = _intervalType.asLiveData() - - private val _oneTime = MutableLiveData(false) - val oneTime = _oneTime.asLiveData() - - private val _initialTitle = MutableLiveData() - val initialTitle = _initialTitle.asLiveData() - - private val _currency = MutableLiveData() - val currency = _currency.asLiveData() - - private val _description = MutableLiveData() - val description = _description.asLiveData() - - private val _accounts = MutableLiveData>() - val accounts = _accounts.asLiveData() - - private val _categories = MutableLiveData>() - val categories = _categories.asLiveData() - - private val _account = MutableLiveData() - val account = _account.asLiveData() - - private val _category = MutableLiveData() - val category = _category.asLiveData() - - private val _amount = MutableLiveData(0.0) - val amount = _amount.asLiveData() - - private var loadedRule: PlannedPaymentRule? = null - private var editMode = false - - var title: String? = null - - fun start() { - viewModelScope.launch { - TestIdlingResource.increment() - -// editMode = screen.plannedPaymentRuleId != null - - val accounts = accountsAct(Unit) - if (accounts.isEmpty()) { - - return@launch - } - _accounts.value = accounts - - _categories.value = categoriesAct(Unit)!! - - reset() - -// loadedRule = screen.plannedPaymentRuleId?.let { -// ioThread { plannedPaymentRuleDao.findById(it)!!.toDomain() } -// } ?: PlannedPaymentRule( -// startDate = null, -// intervalN = null, -// intervalType = null, -// oneTime = false, -// type = screen.type, -// amount = screen.amount ?: 0.0, -// accountId = screen.accountId ?: accounts.first().id, -// categoryId = screen.categoryId, -// title = screen.title, -// description = screen.description -// ) - - display(loadedRule!!) - - TestIdlingResource.decrement() - } - } - - private suspend fun display(rule: PlannedPaymentRule) { - this.title = rule.title - - _transactionType.value = rule.type - _startDate.value = rule.startDate - _intervalN.value = rule.intervalN - _oneTime.value = rule.oneTime - _intervalType.value = rule.intervalType - _initialTitle.value = rule.title - _description.value = rule.description - val selectedAccount = ioThread { accountDao.findById(rule.accountId)!!.toDomain() } - _account.value = selectedAccount - _category.value = rule.categoryId?.let { - ioThread { categoryDao.findById(rule.categoryId!!)?.toDomain() } - } - _amount.value = rule.amount - - updateCurrency(account = selectedAccount) - } - - private suspend fun updateCurrency(account: AccountOld) { - _currency.value = account.currency ?: baseCurrency() - } - - private suspend fun baseCurrency(): String = - ioThread { settingsDao.findFirstSuspend().currency } - - - fun onRuleChanged( - startDate: LocalDateTime, - oneTime: Boolean, - intervalN: Int?, - intervalType: IntervalType? - ) { - loadedRule = loadedRule().copy( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime - ) - _startDate.value = startDate - _intervalN.value = intervalN - _intervalType.value = intervalType - _oneTime.value = oneTime - - saveIfEditMode() - } - - fun onAmountChanged(newAmount: Double) { - loadedRule = loadedRule().copy( - amount = newAmount - ) - _amount.value = newAmount - - saveIfEditMode() - } - - fun onTitleChanged(newTitle: String?) { - loadedRule = loadedRule().copy( - title = newTitle - ) - this.title = newTitle - - saveIfEditMode() - } - - fun onDescriptionChanged(newDescription: String?) { - loadedRule = loadedRule().copy( - description = newDescription - ) - _description.value = newDescription - - saveIfEditMode() - } - - fun onCategoryChanged(newCategory: CategoryOld?) { - loadedRule = loadedRule().copy( - categoryId = newCategory?.id - ) - _category.value = newCategory - - saveIfEditMode() - } - - fun onAccountChanged(newAccount: AccountOld) { - loadedRule = loadedRule().copy( - accountId = newAccount.id - ) - _account.value = newAccount - - viewModelScope.launch { - updateCurrency(account = newAccount) - } - - saveIfEditMode() - } - - fun onSetTransactionType(newTransactionType: TrnTypeOld) { - loadedRule = loadedRule().copy( - type = newTransactionType - ) - _transactionType.value = newTransactionType - - saveIfEditMode() - } - - private fun saveIfEditMode() { - if (editMode) { - save(false) - } - } - - fun save(closeScreen: Boolean = true) { - if (!validate()) { - return - } - - viewModelScope.launch { - TestIdlingResource.increment() - - try { - ioThread { - loadedRule = loadedRule().copy( - type = transactionType.value ?: error("no transaction type"), - startDate = startDate.value ?: error("no startDate"), - intervalN = intervalN.value ?: error("no intervalN"), - intervalType = intervalType.value ?: error("no intervalType"), - categoryId = category.value?.id, - accountId = account.value?.id ?: error("no accountId"), - title = title?.trim(), - description = description.value?.trim(), - amount = amount.value ?: error("no amount"), - - isSynced = false - ) - - plannedPaymentRuleDao.save(loadedRule().toEntity()) - plannedPaymentsGenerator.generate(loadedRule()) - } - - if (closeScreen) { - - - ioThread { - plannedPaymentRuleUploader.sync(loadedRule()) - transactionSync.sync() - } - } - } catch (e: Exception) { - e.printStackTrace() - } - - TestIdlingResource.decrement() - } - } - - private fun validate(): Boolean { - if (transactionType.value == TrnTypeOld.TRANSFER) { - return false - } - - if (amount.value == 0.0) { - return false - } - - return if (oneTime.value == true) validateOneTime() else validateRecurring() - } - - private fun validateOneTime(): Boolean { - return startDate.value != null - } - - private fun validateRecurring(): Boolean { - return startDate.value != null && - intervalN.value != null && - intervalN.value!! > 0 && - intervalType.value != null - } - - fun delete() { - viewModelScope.launch { - ioThread { - loadedRule?.let { - plannedPaymentRuleDao.flagDeleted(it.id) - transactionDao.flagDeletedByRecurringRuleIdAndNoDateTime( - recurringRuleId = it.id - ) - } - - - loadedRule?.let { - plannedPaymentRuleUploader.delete(it.id) - transactionSync.sync() - } - } - } - } - - fun createCategory(data: CreateCategoryData) { - viewModelScope.launch { - categoryCreator.createCategory(data) { - _categories.value = categoriesAct(Unit)!! - - onCategoryChanged(it) - } - } - } - - fun createAccount(data: CreateAccountData) { - viewModelScope.launch { - accountCreator.createAccount(data) { - EventBus.getDefault().post(AccountsUpdatedEvent()) - _accounts.value = accountsAct(Unit)!! - } - } - } - - private fun reset() { - loadedRule = null - - _initialTitle.value = null - _description.value = null - _category.value = null - } - - private fun loadedRule() = loadedRule ?: error("Loaded transaction is null") -} \ No newline at end of file diff --git a/planned-payments/src/main/java/com/ivy/planned/edit/RecurringRule.kt b/planned-payments/src/main/java/com/ivy/planned/edit/RecurringRule.kt deleted file mode 100644 index 1917a58b8d..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/edit/RecurringRule.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.ivy.wallet.ui.planned.edit - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.forDisplay -import com.ivy.data.planned.IntervalType -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Orange -import com.ivy.wallet.ui.theme.components.AddPrimaryAttributeButton -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.utils.formatDateOnly -import com.ivy.wallet.utils.timeNowUTC -import com.ivy.wallet.utils.uppercaseLocal -import java.time.LocalDateTime - -@Composable -fun RecurringRule( - startDate: LocalDateTime?, - intervalN: Int?, - intervalType: IntervalType?, - oneTime: Boolean, - onShowRecurringRuleModal: () -> Unit, -) { - if ( - hasRecurringRule( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime - ) - ) { - RecurringRuleCard( - startDate = startDate!!, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime, - onClick = { - onShowRecurringRuleModal() - } - ) - } else { - AddPrimaryAttributeButton( - icon = R.drawable.ic_planned_payments, - text = stringResource(R.string.add_planned_date_payment), - onClick = onShowRecurringRuleModal - ) - } -} - -fun hasRecurringRule( - startDate: LocalDateTime?, - intervalN: Int?, - intervalType: IntervalType?, - oneTime: Boolean, -): Boolean { - return startDate != null && - ((intervalN != null && intervalType != null) || oneTime) -} - -@Composable -private fun RecurringRuleCard( - startDate: LocalDateTime, - intervalN: Int?, - intervalType: IntervalType?, - oneTime: Boolean, - onClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .background(UI.colors.medium, UI.shapes.squared) - .clickable(onClick = onClick) - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon(icon = R.drawable.ic_planned_payments) - - Spacer(Modifier.width(8.dp)) - - Column { - Text( - text = if (oneTime) stringResource(R.string.planned_for) else stringResource(R.string.planned_start_at), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - if (!oneTime && intervalType != null && intervalN != null) { - Spacer(Modifier.height(4.dp)) - - val intervalTypeLabel = intervalType.forDisplay(intervalN).uppercaseLocal() - Text( - text = stringResource(R.string.repeats_every, intervalN, intervalTypeLabel), - style = UI.typo.c.style( - fontWeight = FontWeight.ExtraBold, - color = Orange - ) - ) - } - } - - Spacer(Modifier.weight(1f)) - - Text( - text = startDate.toLocalDate().formatDateOnly(), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } -} - -@Preview -@Composable -private fun Preview_Empty() { - ComponentPreview { - RecurringRule( - startDate = null, - intervalN = null, - intervalType = null, - oneTime = true - ) { - } - } -} - -@Preview -@Composable -private fun Preview_Repeat() { - ComponentPreview { - RecurringRule( - startDate = timeNowUTC(), - intervalN = 1, - intervalType = IntervalType.MONTH, - oneTime = false - ) { - } - } -} - -@Preview -@Composable -private fun Preview_OneTime() { - ComponentPreview { - RecurringRule( - startDate = timeNowUTC().plusDays(5), - intervalN = null, - intervalType = null, - oneTime = true - ) { - } - } -} \ No newline at end of file diff --git a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentCard.kt b/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentCard.kt deleted file mode 100644 index 9abdae077c..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentCard.kt +++ /dev/null @@ -1,341 +0,0 @@ -package com.ivy.wallet.ui.planned.list - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.forDisplay -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.planned.IntervalType -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.ui.component.transaction.TypeAmountCurrency -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.components.getCustomIconIdS -import com.ivy.wallet.utils.* -import java.time.LocalDateTime - -@Composable -fun LazyItemScope.PlannedPaymentCard( - baseCurrency: String, - categories: List, - accounts: List, - plannedPayment: PlannedPaymentRule, - onClick: (PlannedPaymentRule) -> Unit, -) { - Spacer(Modifier.height(12.dp)) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .clickable { - if (accounts.find { it.id == plannedPayment.accountId } != null) { - onClick(plannedPayment) - } - } - .background(UI.colors.medium, UI.shapes.squared) - .testTag("planned_payment_card") - ) { - val currency = accounts.find { it.id == plannedPayment.accountId }?.currency ?: baseCurrency - - Spacer(Modifier.height(20.dp)) - - PlannedPaymentHeaderRow( - plannedPayment = plannedPayment, - categories = categories, - accounts = accounts - ) - - Spacer(Modifier.height(16.dp)) - - RuleTextRow( - oneTime = plannedPayment.oneTime, - startDate = plannedPayment.startDate, - intervalN = plannedPayment.intervalN, - intervalType = plannedPayment.intervalType - ) - - if (plannedPayment.title.isNotNullOrBlank()) { - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = plannedPayment.title!!, - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - } - - Spacer(Modifier.height(20.dp)) - - TypeAmountCurrency( - transactionType = plannedPayment.type, - dueDate = null, - currency = currency, - amount = plannedPayment.amount - ) - - Spacer(Modifier.height(24.dp)) - } -} - -@Composable -private fun PlannedPaymentHeaderRow( - plannedPayment: PlannedPaymentRule, - categories: List, - accounts: List -) { - - - if (plannedPayment.type != TrnTypeOld.TRANSFER) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - IvyIcon( - modifier = Modifier - .background(UI.colors.pure, CircleShape), - icon = R.drawable.ic_planned_payments, - tint = UI.colorsInverted.pure - ) - - Spacer(Modifier.width(12.dp)) - - val category = - plannedPayment.categoryId?.let { targetId -> categories.find { it.id == targetId } } - if (category != null) { - IvyButton( - iconTint = findContrastTextColor(category.color.toComposeColor()), - iconStart = getCustomIconIdS(category.icon, R.drawable.ic_custom_category_s), - text = category.name, - backgroundGradient = Gradient.solid(category.color.toComposeColor()), - textStyle = UI.typo.c.style( - color = findContrastTextColor(category.color.toComposeColor()), - fontWeight = FontWeight.ExtraBold - ), - padding = 8.dp, - iconEdgePadding = 10.dp - ) { -// nav.navigateTo( -// ItemStatistic( -// accountId = null, -// categoryId = category.id -// ) -// ) - } - - Spacer(Modifier.width(12.dp)) - } - - val account = accounts.find { it.id == plannedPayment.accountId } - IvyButton( - backgroundGradient = Gradient.solid(UI.colors.pure), - text = account?.name ?: stringResource(R.string.deleted), - iconTint = UI.colorsInverted.pure, - iconStart = getCustomIconIdS(account?.icon, R.drawable.ic_custom_account_s), - textStyle = UI.typo.c.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ), - padding = 8.dp, - iconEdgePadding = 10.dp - ) { - account?.let { -// nav.navigateTo( -// ItemStatistic( -// accountId = account.id, -// categoryId = null -// ) -// ) - } - } - } - } -} - -@Composable -private fun RuleTextRow( - oneTime: Boolean, - startDate: LocalDateTime?, - intervalN: Int?, - intervalType: IntervalType? -) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - if (oneTime) { - Text( - text = stringResource(R.string.planned_for_uppercase), - style = UI.typoSecond.c.style( - color = Orange, - fontWeight = FontWeight.SemiBold - ) - ) - Text( - modifier = Modifier.padding(bottom = 1.dp), - text = startDate?.toLocalDate()?.formatDateOnlyWithYear()?.uppercaseLocal() - ?: stringResource(R.string.null_text), - style = UI.typoSecond.c.style( - color = Orange, - fontWeight = FontWeight.ExtraBold - ) - ) - } else { - val startDateFormatted = startDate?.toLocalDate()?.formatDateOnly()?.uppercaseLocal() - Text( - text = stringResource(R.string.starts_date, startDateFormatted ?: ""), - style = UI.typoSecond.c.style( - color = Orange, - fontWeight = FontWeight.SemiBold - ) - ) - val intervalTypeFormatted = intervalType?.forDisplay(intervalN ?: 0)?.uppercaseLocal() - Text( - modifier = Modifier.padding(bottom = 1.dp), - text = stringResource( - R.string.repeats_every, - intervalN ?: 0, - intervalTypeFormatted ?: "" - ), - style = UI.typoSecond.c.style( - color = Orange, - fontWeight = FontWeight.ExtraBold - ) - ) - } - - Spacer(Modifier.width(24.dp)) - - } -} - -@Preview -@Composable -private fun Preview_oneTime() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val cash = AccountOld(name = "Cash", color = Green.toArgb()) - val food = CategoryOld(name = "Food", color = Green.toArgb()) - - item { - Spacer(Modifier.height(68.dp)) - - PlannedPaymentCard( - baseCurrency = "BGN", - categories = listOf(food), - accounts = listOf(cash), - plannedPayment = PlannedPaymentRule( - accountId = cash.id, - title = "Lidl pazar", - categoryId = food.id, - amount = 250.75, - startDate = timeNowUTC().plusDays(5), - oneTime = true, - intervalType = null, - intervalN = null, - type = TrnTypeOld.EXPENSE - ) - ) { - - } - } - } - } -} - -@Preview -@Composable -private fun Preview_recurring() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val account = AccountOld(name = "Revolut", color = Green.toArgb()) - val shisha = CategoryOld(name = "Shisha", color = Orange.toArgb()) - - item { - Spacer(Modifier.height(68.dp)) - - PlannedPaymentCard( - baseCurrency = "BGN", - categories = listOf(shisha), - accounts = listOf(account), - plannedPayment = PlannedPaymentRule( - accountId = account.id, - title = "Tabu", - categoryId = shisha.id, - amount = 250.75, - startDate = timeNowUTC().plusDays(5), - oneTime = false, - intervalType = IntervalType.MONTH, - intervalN = 1, - type = TrnTypeOld.EXPENSE - ) - ) { - - } - } - } - } -} - -@Preview -@Composable -private fun Preview_recurringError() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val account = AccountOld(name = "Revolut", color = Green.toArgb()) - val shisha = CategoryOld(name = "Shisha", color = Orange.toArgb()) - - item { - Spacer(Modifier.height(68.dp)) - - PlannedPaymentCard( - baseCurrency = "BGN", - categories = listOf(shisha), - accounts = listOf(account), - plannedPayment = PlannedPaymentRule( - accountId = account.id, - title = "Tabu", - categoryId = shisha.id, - amount = 250.75, - startDate = timeNowUTC().plusDays(5), - oneTime = false, - intervalType = null, - intervalN = null, - type = TrnTypeOld.EXPENSE - ) - ) { - - } - } - } - } -} diff --git a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsBottomBar.kt b/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsBottomBar.kt deleted file mode 100644 index b1cbca6a83..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsBottomBar.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.ivy.wallet.ui.planned.list - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.components.ActionsRow -import com.ivy.wallet.ui.theme.components.CloseButton -import com.ivy.wallet.ui.theme.components.IvyOutlinedButton -import com.ivy.wallet.ui.theme.gradientCutBackgroundTop -import com.ivy.wallet.utils.navigationBarInset -import com.ivy.wallet.utils.toDensityDp - -@Composable -fun BoxWithConstraintsScope.PlannedPaymentsBottomBar( - bottomInset: Dp = navigationBarInset().toDensityDp(), - onClose: () -> Unit, - onAdd: () -> Unit -) { - ActionsRow( - modifier = Modifier - .align(Alignment.BottomCenter) - .gradientCutBackgroundTop() - .padding(bottom = bottomInset) - .padding(bottom = 24.dp) - ) { - Spacer(Modifier.width(20.dp)) - - CloseButton { - onClose() - } - - Spacer(Modifier.weight(1f)) - - IvyOutlinedButton( - iconStart = R.drawable.ic_planned_payments, - text = stringResource(R.string.add_payment), - solidBackground = true - ) { - onAdd() - } - - Spacer(Modifier.width(20.dp)) - } -} - -@Preview -@Composable -private fun PreviewBottomBar() { - IvyPreview { - PlannedPaymentsBottomBar( - bottomInset = 16.dp, - onAdd = {}, - onClose = {} - ) - } -} \ No newline at end of file diff --git a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsLazyColumn.kt b/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsLazyColumn.kt deleted file mode 100644 index 1454fd75a0..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsLazyColumn.kt +++ /dev/null @@ -1,229 +0,0 @@ -package com.ivy.wallet.ui.planned.list - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style - - -import com.ivy.old.component.transaction.SectionDivider -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.IvyIcon -import kotlin.math.absoluteValue - -@Composable -fun PlannedPaymentsLazyColumn( - Header: @Composable () -> Unit, - - - currency: String, - categories: List, - accounts: List, - listState: LazyListState = rememberLazyListState(), - - oneTime: List, - oneTimeIncome: Double, - oneTimeExpenses: Double, - - - recurring: List, - recurringIncome: Double, - recurringExpenses: Double, -) { - - var oneTimeExpanded by remember { mutableStateOf(true) } - var recurringExpanded by remember { mutableStateOf(true) } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - ) { - item { - Header() - } - - plannedPaymentItems( - - currency = currency, - categories = categories, - accounts = accounts, - listState = listState, - - oneTime = oneTime, - oneTimeIncome = oneTimeIncome, - oneTimeExpenses = oneTimeExpenses, - oneTimeExpanded = oneTimeExpanded, - setOneTimeExpanded = { - oneTimeExpanded = it - }, - - recurring = recurring, - recurringIncome = recurringIncome, - recurringExpenses = recurringExpenses, - recurringExpanded = recurringExpanded, - setRecurringExpanded = { - recurringExpanded = it - } - ) - } -} - -private fun LazyListScope.plannedPaymentItems( - - currency: String, - categories: List, - accounts: List, - listState: LazyListState, - - oneTime: List, - oneTimeIncome: Double, - oneTimeExpenses: Double, - oneTimeExpanded: Boolean, - setOneTimeExpanded: (Boolean) -> Unit, - - - recurring: List, - recurringIncome: Double, - recurringExpenses: Double, - recurringExpanded: Boolean, - setRecurringExpanded: (Boolean) -> Unit -) { - if (oneTime.isNotEmpty()) { - - item { - SectionDivider( - expanded = oneTimeExpanded, - setExpanded = setOneTimeExpanded, - title = stringResource(R.string.one_time_payments), - titleColor = UI.colorsInverted.pure, - baseCurrency = currency, - income = oneTimeIncome, - expenses = oneTimeExpenses.absoluteValue - ) - } - - if (oneTimeExpanded) { - itemsIndexed(oneTime) { _, item -> - PlannedPaymentCard( - baseCurrency = currency, - categories = categories, - accounts = accounts, - plannedPayment = item, - ) { plannedPaymentRule -> - onPlannedPaymentClick( - - listState = listState, - rule = plannedPaymentRule - ) - } - } - } - } - - if (recurring.isNotEmpty()) { - - item { - SectionDivider( - expanded = recurringExpanded, - setExpanded = setRecurringExpanded, - title = stringResource(R.string.recurring_payments), - titleColor = UI.colorsInverted.pure, - baseCurrency = currency, - income = recurringIncome, - expenses = recurringExpenses.absoluteValue - ) - } - - if (recurringExpanded) { - itemsIndexed(recurring) { _, item -> - PlannedPaymentCard( - baseCurrency = currency, - categories = categories, - accounts = accounts, - plannedPayment = item, - ) { plannedPaymentRule -> - onPlannedPaymentClick( - - listState = listState, - rule = plannedPaymentRule - ) - } - } - } - } - - if (oneTime.isEmpty() && recurring.isEmpty()) { - item { - NoPlannedPaymentsEmptyState() - } - } - - item { - //last spacer - scroll hack - Spacer(Modifier.height(150.dp)) - } -} - -private fun onPlannedPaymentClick( - - listState: LazyListState, - rule: PlannedPaymentRule -) { -// nav.navigateTo( -// EditPlanned( -// plannedPaymentRuleId = rule.id, -// type = rule.type -// ) -// ) -} - - -@Composable -private fun LazyItemScope.NoPlannedPaymentsEmptyState() { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.height(64.dp)) - - IvyIcon( - icon = R.drawable.ic_planned_payments, - tint = Gray - ) - - Spacer(Modifier.height(24.dp)) - - Text( - text = stringResource(R.string.no_planned_payments), - style = UI.typo.b1.style( - color = Gray, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(8.dp)) - - Text( - text = stringResource(R.string.no_planned_payments_description), - style = UI.typo.b2.style( - color = Gray, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center - ) - ) - } -} \ No newline at end of file diff --git a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsScreen.kt b/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsScreen.kt deleted file mode 100644 index a89e471586..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsScreen.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.ivy.wallet.ui.planned.list - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.planned.IntervalType -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - - -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.Orange -import com.ivy.wallet.utils.timeNowUTC - -@Composable -fun BoxWithConstraintsScope.PlannedPaymentsScreen() { - val viewModel: PlannedPaymentsViewModel = hiltViewModel() - - val currency by viewModel.currency.observeAsState("") - val categories by viewModel.categories.observeAsState(emptyList()) - val accounts by viewModel.accounts.observeAsState(emptyList()) - val oneTime by viewModel.oneTime.observeAsState(emptyList()) - val oneTimeIncome by viewModel.oneTimeIncome.observeAsState(0.0) - val oneTimeExpenses by viewModel.oneTimeExpenses.observeAsState(0.0) - val recurring by viewModel.recurring.observeAsState(emptyList()) - val recurringIncome by viewModel.recurringIncome.observeAsState(0.0) - val recurringExpenses by viewModel.recurringExpenses.observeAsState(0.0) - - UI( - currency = currency, - categories = categories, - accounts = accounts, - oneTime = oneTime, - oneTimeIncome = oneTimeIncome, - oneTimeExpenses = oneTimeExpenses, - recurring = recurring, - recurringIncome = recurringIncome, - recurringExpenses = recurringExpenses - ) -} - -@Composable -private fun BoxWithConstraintsScope.UI( - currency: String, - - categories: List, - accounts: List, - - oneTime: List, - oneTimeIncome: Double, - oneTimeExpenses: Double, - - recurring: List, - recurringIncome: Double, - recurringExpenses: Double -) { - PlannedPaymentsLazyColumn( - Header = { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(start = 24.dp), - text = stringResource(R.string.planned_payments_inline), - style = UI.typo.h2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.height(24.dp)) - }, - - currency = currency, - categories = categories, - accounts = accounts, - oneTime = oneTime, - oneTimeIncome = oneTimeIncome, - oneTimeExpenses = oneTimeExpenses, - recurring = recurring, - recurringIncome = recurringIncome, - recurringExpenses = recurringExpenses - ) - - - PlannedPaymentsBottomBar( - onClose = { - - }, - onAdd = { -// nav.navigateTo( -// EditPlanned( -// type = TrnTypeOld.EXPENSE, -// plannedPaymentRuleId = null -// ) -// ) - } - ) -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - val account = AccountOld(name = "Cash", color = Green.toArgb()) - val food = CategoryOld(name = "Food", color = Ivy.toArgb()) - val shisha = CategoryOld(name = "Shisha", color = Orange.toArgb()) - - UI( - currency = "BGN", - accounts = listOf(account), - categories = listOf(food, shisha), - - oneTime = listOf( - PlannedPaymentRule( - accountId = account.id, - title = "Lidl pazar", - categoryId = food.id, - amount = 250.75, - startDate = timeNowUTC().plusDays(5), - oneTime = true, - intervalType = null, - intervalN = null, - type = TrnTypeOld.EXPENSE - ) - ), - oneTimeExpenses = 250.75, - oneTimeIncome = 0.0, - recurring = listOf( - PlannedPaymentRule( - accountId = account.id, - title = "Tabu", - categoryId = shisha.id, - amount = 1025.5, - startDate = timeNowUTC().plusDays(5), - oneTime = false, - intervalType = IntervalType.MONTH, - intervalN = 1, - type = TrnTypeOld.EXPENSE - ) - ), - recurringExpenses = 1025.5, - recurringIncome = 0.0 - ) - } -} \ No newline at end of file diff --git a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsViewModel.kt b/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsViewModel.kt deleted file mode 100644 index f03fcf241c..0000000000 --- a/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsViewModel.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.ivy.wallet.ui.planned.list - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.frp.test.TestIdlingResource -import com.ivy.wallet.domain.action.account.AccountsActOld -import com.ivy.wallet.domain.action.category.CategoriesActOld -import com.ivy.wallet.io.persistence.dao.AccountDao -import com.ivy.wallet.io.persistence.dao.CategoryDao -import com.ivy.wallet.io.persistence.dao.SettingsDao -import com.ivy.wallet.utils.asLiveData -import com.ivy.wallet.utils.ioThread -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class PlannedPaymentsViewModel @Inject constructor( - private val settingsDao: SettingsDao, - private val categoryDao: CategoryDao, - private val accountDao: AccountDao, - private val plannedPaymentsLogic: PlannedPaymentsLogic, - private val categoriesAct: CategoriesActOld, - private val accountsAct: AccountsActOld -) : ViewModel() { - - private val _currency = MutableLiveData() - val currency = _currency.asLiveData() - - private val _categories = MutableLiveData>() - val categories = _categories.asLiveData() - - private val _accounts = MutableLiveData>() - val accounts = _accounts.asLiveData() - - //One Time - private val _oneTime = MutableLiveData>() - val oneTime = _oneTime.asLiveData() - - private val _oneTimeIncome = MutableLiveData() - val oneTimeIncome = _oneTimeIncome.asLiveData() - - private val _oneTimeExpenses = MutableLiveData() - val oneTimeExpenses = _oneTimeExpenses.asLiveData() - - //Recurring - private val _recurring = MutableLiveData>() - val recurring = _recurring.asLiveData() - - private val _recurringIncome = MutableLiveData() - val recurringIncome = _recurringIncome.asLiveData() - - private val _recurringExpenses = MutableLiveData() - val recurringExpenses = _recurringExpenses.asLiveData() - - fun start() { - viewModelScope.launch { - TestIdlingResource.increment() - - val settings = ioThread { settingsDao.findFirstSuspend() } - - _currency.value = settings.currency - - _categories.value = categoriesAct(Unit)!! - _accounts.value = accountsAct(Unit)!! - - _oneTime.value = ioThread { plannedPaymentsLogic.oneTime() }!! - _oneTimeIncome.value = ioThread { plannedPaymentsLogic.oneTimeIncome() }!! - _oneTimeExpenses.value = ioThread { plannedPaymentsLogic.oneTimeExpenses() }!! - - _recurring.value = ioThread { plannedPaymentsLogic.recurring() }!! - _recurringIncome.value = ioThread { plannedPaymentsLogic.recurringIncome() }!! - _recurringExpenses.value = ioThread { plannedPaymentsLogic.recurringExpenses() }!! - - TestIdlingResource.decrement() - } - } -} \ No newline at end of file diff --git a/reports/.gitignore b/reports/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/reports/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/reports/README.md b/reports/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/reports/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/reports/build.gradle.kts b/reports/build.gradle.kts deleted file mode 100644 index 11ef563d20..0000000000 --- a/reports/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) -} \ No newline at end of file diff --git a/reports/src/main/AndroidManifest.xml b/reports/src/main/AndroidManifest.xml deleted file mode 100644 index 881008e56f..0000000000 --- a/reports/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/reports/src/main/java/com/ivy/reports/FilterOverlay.kt b/reports/src/main/java/com/ivy/reports/FilterOverlay.kt deleted file mode 100644 index 64f46d48e7..0000000000 --- a/reports/src/main/java/com/ivy/reports/FilterOverlay.kt +++ /dev/null @@ -1,890 +0,0 @@ -package com.ivy.reports - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.layout -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.old.ListItem -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.* -import com.ivy.wallet.ui.theme.modal.AddKeywordModal -import com.ivy.wallet.ui.theme.modal.AddModalBackHandling -import com.ivy.wallet.ui.theme.modal.ChoosePeriodModal -import com.ivy.wallet.ui.theme.modal.ChoosePeriodModalData -import com.ivy.wallet.ui.theme.modal.edit.AmountModal -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1Row -import com.ivy.wallet.utils.capitalizeLocal -import com.ivy.wallet.utils.springBounce -import java.util.* -import kotlin.math.roundToInt - -@Composable -fun BoxWithConstraintsScope.FilterOverlay( - visible: Boolean, - - baseCurrency: String, - accounts: List, - categories: List, - - filter: ReportFilter?, - onClose: () -> Unit, - onSetFilter: (ReportFilter?) -> Unit -) { - val percentVisible by animateFloatAsState( - targetValue = if (visible) 1f else 0f, - animationSpec = springBounce() - ) - - var localFilter by remember(filter) { - mutableStateOf(filter) - } - val setLocalFilter = { newFilter: ReportFilter -> - localFilter = newFilter - } - val baseFilter = remember(baseCurrency) { - ReportFilter.emptyFilter(baseCurrency = baseCurrency) - } - val nonNullFilter = { currentFilter: ReportFilter? -> - ReportFilter - currentFilter ?: baseFilter - } - - var choosePeriodModal: ChoosePeriodModalData? by remember { - mutableStateOf(null) - } - var minAmountModalShown by remember { mutableStateOf(false) } - var maxAmountModalShown by remember { mutableStateOf(false) } - var includeKeywordModalShown by remember { mutableStateOf(false) } - var excludeKeywordModalShown by remember { mutableStateOf(false) } - - val includesKeywordId by remember(includeKeywordModalShown) { - mutableStateOf(UUID.randomUUID()) - } - - val excludesKeywordId by remember(excludeKeywordModalShown) { - mutableStateOf(UUID.randomUUID()) - } - - if (percentVisible > 0.01f) { - Column( - modifier = Modifier - .fillMaxSize() - .zIndex(100f) - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - layout( - placeable.width, placeable.height - ) { - placeable.place( - x = 0, - y = -(placeable.height * (1f - percentVisible)).roundToInt() - ) - } - } - .background(UI.colors.pure) - .systemBarsPadding() - .verticalScroll(rememberScrollState()), - ) { - val modalId = remember { - UUID.randomUUID() - } - - AddModalBackHandling( - modalId = modalId, - visible = visible - ) { - onClose() - } - - Spacer(Modifier.height(24.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.padding( - start = 32.dp - ), - text = stringResource(R.string.filter), - style = UI.typo.h2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.weight(1f)) - - Text( - modifier = Modifier - .clickable { - localFilter = null - onSetFilter(null) - } - .padding(all = 4.dp), //expand click area - text = stringResource(R.string.clean_filter), - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - color = Color.Gray - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - - Spacer(Modifier.height(24.dp)) - - TypeFilter( - filter = localFilter, - nonNullFilter = nonNullFilter, - onSetFilter = setLocalFilter - ) - - FilterDivider() - - PeriodFilter( - filter = localFilter, - onShowPeriodChooserModal = { - choosePeriodModal = ChoosePeriodModalData( - period = filter?.period!! - ) - } - ) - - FilterDivider() - - AccountsFilter( - allAccounts = accounts, - filter = localFilter, - nonNullFilter = nonNullFilter, - onSetFilter = setLocalFilter - ) - - FilterDivider() - - CategoriesFilter( - allCategories = categories, - filter = localFilter, - nonNullFilter = nonNullFilter, - onSetFilter = setLocalFilter - ) - - FilterDivider() - - AmountFilter( - baseCurrency = baseCurrency, - filter = localFilter, - onShowMinAmountModal = { - minAmountModalShown = true - }, - onShowMaxAmountModal = { - maxAmountModalShown = true - } - ) - - FilterDivider() - - KeywordsFilter( - filter = localFilter, - onSetFilter = setLocalFilter, - nonNullFilter = nonNullFilter, - onShowIncludeKeywordModal = { - includeKeywordModalShown = true - }, - onShowExcludeKeywordModal = { - excludeKeywordModalShown = true - } - ) - - Spacer(Modifier.height(196.dp)) - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .alpha(percentVisible) - .align(Alignment.BottomCenter) - .zIndex(200f) - .padding(bottom = 32.dp) - ) { - Spacer(Modifier.width(24.dp)) - - CloseButton { - onClose() - } - - Spacer(Modifier.weight(1f)) - - IvyButton( - text = stringResource(R.string.apply_filter), - iconStart = R.drawable.ic_filter_xs, - backgroundGradient = GradientGreen, - padding = 10.dp, - ) { - if (localFilter != null) { - onSetFilter(localFilter!!) - } - onClose() - } - - Spacer(Modifier.width(24.dp)) - } - - if (percentVisible > 0.01f) { - GradientCutBottom( - height = 196.dp, - alpha = percentVisible, - zIndex = 150f - ) - } - - ChoosePeriodModal( - modal = choosePeriodModal, - dismiss = { choosePeriodModal = null }, - ) { selectedPeriod -> - localFilter = nonNullFilter(localFilter).copy( - period = selectedPeriod - ) - } - - val minAmountModalId = remember( - nonNullFilter(filter).id, - nonNullFilter(filter).minAmount - ) { - UUID.randomUUID() - } - AmountModal( - id = minAmountModalId, - visible = minAmountModalShown, - currency = baseCurrency, - initialAmount = filter?.minAmount?.takeIf { it > 0 }, - dismiss = { - minAmountModalShown = false - } - ) { - localFilter = nonNullFilter(localFilter).copy( - minAmount = it.takeIf { it > 0 } - ) - } - - val maxAmountModalId = remember( - nonNullFilter(localFilter).id, - nonNullFilter(localFilter).maxAmount - ) { - UUID.randomUUID() - } - AmountModal( - id = maxAmountModalId, - visible = maxAmountModalShown, - currency = baseCurrency, - initialAmount = filter?.maxAmount?.takeIf { it > 0 }, - dismiss = { - maxAmountModalShown = false - } - ) { - localFilter = nonNullFilter(localFilter).copy( - maxAmount = it.takeIf { it > 0 } - ) - } - - AddKeywordModal( - id = includesKeywordId, - keyword = "", - visible = includeKeywordModalShown, - dismiss = { includeKeywordModalShown = false } - ) { keyword -> - localFilter = nonNullFilter(localFilter).copy( - includeKeywords = nonNullFilter(localFilter) - .includeKeywords.plus(keyword) - .toSet().toList() //filter duplicated - ) - } - - AddKeywordModal( - id = excludesKeywordId, - keyword = "", - visible = excludeKeywordModalShown, - dismiss = { excludeKeywordModalShown = false } - ) { keyword -> - localFilter = nonNullFilter(localFilter).copy( - excludeKeywords = nonNullFilter(localFilter) - .excludeKeywords.plus(keyword) - .toSet().toList() //filter duplicated - ) - } -} - -@Composable -private fun TypeFilter( - filter: ReportFilter?, - nonNullFilter: (ReportFilter?) -> ReportFilter, - onSetFilter: (ReportFilter) -> Unit -) { - FilterTitleText( - text = stringResource(R.string.by_type), - active = filter != null && filter.trnTypes.isNotEmpty(), - inactiveColor = Red - ) - - Spacer(Modifier.height(12.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - TypeFilterCheckbox( - trnType = TrnTypeOld.INCOME, - filter = filter, - nonFilter = nonNullFilter, - onSetFilter = onSetFilter - ) - - Spacer(Modifier.width(20.dp)) - - TypeFilterCheckbox( - trnType = TrnTypeOld.EXPENSE, - filter = filter, - nonFilter = nonNullFilter, - onSetFilter = onSetFilter - ) - } - - Spacer(Modifier.height(4.dp)) - - TypeFilterCheckbox( - modifier = Modifier.padding(start = 20.dp), - trnType = TrnTypeOld.TRANSFER, - filter = filter, - nonFilter = nonNullFilter, - onSetFilter = onSetFilter - ) -} - -@Composable -private fun TypeFilterCheckbox( - modifier: Modifier = Modifier, - trnType: TrnTypeOld, - filter: ReportFilter?, - nonFilter: (ReportFilter?) -> ReportFilter, - onSetFilter: (ReportFilter) -> Unit -) { - IvyCheckboxWithText( - modifier = modifier, - text = when (trnType) { - TrnTypeOld.INCOME -> stringResource(R.string.incomes) - TrnTypeOld.EXPENSE -> stringResource(R.string.expenses) - TrnTypeOld.TRANSFER -> stringResource(R.string.account_transfers) - }, - checked = filter != null && filter.trnTypes.contains(trnType), - ) { checked -> - if (checked) { - //remove trn type - onSetFilter( - nonFilter(filter).copy( - trnTypes = nonFilter(filter).trnTypes.plus(trnType) - ) - ) - } else { - //add trn type - onSetFilter( - nonFilter(filter).copy( - trnTypes = nonFilter(filter).trnTypes.filter { it != trnType } - ) - ) - } - - } -} - -@Composable -private fun PeriodFilter( - filter: ReportFilter?, - onShowPeriodChooserModal: () -> Unit -) { - FilterTitleText( - text = stringResource(R.string.time_period), - active = filter?.period != null, - inactiveColor = Red - ) - - Spacer(Modifier.height(16.dp)) - - IvyOutlinedButtonFillMaxWidth( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - iconStart = R.drawable.ic_calendar, - text = filter?.period?.toDisplayLong(1) - ?.capitalizeLocal() - ?: stringResource(R.string.select_time_range), - padding = 12.dp, - ) { - onShowPeriodChooserModal() - } -} - -@Composable -private fun AccountsFilter( - allAccounts: List, - filter: ReportFilter?, - nonNullFilter: (ReportFilter?) -> ReportFilter, - onSetFilter: (ReportFilter) -> Unit -) { - ListFilterTitle( - text = stringResource(R.string.accounts_number, filter?.accounts?.size ?: 0), - active = filter != null && filter.accounts.isNotEmpty(), - itemsSelected = filter?.accounts?.size ?: 0, - onClearAll = { - onSetFilter( - nonNullFilter(filter).copy( - accounts = emptyList() - ) - ) - }, - onSelectAll = { - onSetFilter( - nonNullFilter(filter).copy( - accounts = allAccounts - ) - ) - } - ) - - Spacer(Modifier.height(16.dp)) - - LazyRow { - item { - Spacer(Modifier.width(24.dp)) - } - - items(items = allAccounts) { account -> - ListItem( - icon = account.icon, - defaultIcon = R.drawable.ic_custom_account_s, - text = account.name, - selectedColor = account.color.toComposeColor().takeIf { - filter?.accounts?.contains(account) == true - } - ) { selected -> - if (selected) { - //remove account - onSetFilter( - nonNullFilter(filter).copy( - accounts = nonNullFilter(filter).accounts.filter { it != account } - ) - ) - } else { - //add account - onSetFilter( - nonNullFilter(filter).copy( - accounts = nonNullFilter(filter).accounts - .plus(account).sortedBy { it.orderNum } - ) - ) - } - } - } - - item { - Spacer(Modifier.width(24.dp)) - } - } -} - -@Composable -private fun CategoriesFilter( - allCategories: List, - filter: ReportFilter?, - nonNullFilter: (ReportFilter?) -> ReportFilter, - onSetFilter: (ReportFilter) -> Unit -) { - val myNonNullFilter = nonNullFilter(filter) - val selectedItemsCount = filter?.categories?.size ?: 0 - - ListFilterTitle( - text = stringResource(R.string.categories_number, selectedItemsCount), - active = filter != null && filter.categories.isNotEmpty(), - itemsSelected = selectedItemsCount, - onClearAll = { - onSetFilter( - myNonNullFilter.copy( - categories = emptyList() - ) - ) - }, - onSelectAll = { - onSetFilter( - myNonNullFilter.copy( - categories = allCategories - ) - ) - } - ) - - Spacer(Modifier.height(16.dp)) - - LazyRow { - item { - Spacer(Modifier.width(24.dp)) - } - - items(items = allCategories) { category -> - ListItem( - icon = category.icon, - defaultIcon = R.drawable.ic_custom_category_s, - text = category.name, - selectedColor = category.color.toComposeColor().takeIf { - filter?.categories?.contains(category) == true - } - ) { selected -> - if (selected) { - //remove category - onSetFilter( - myNonNullFilter.copy( - categories = myNonNullFilter.categories.filter { it != category } - ) - ) - } else { - //add category - onSetFilter( - myNonNullFilter.copy( - categories = myNonNullFilter.categories - .plus(category).sortedBy { it.orderNum } - ) - ) - } - } - } - - item { - Spacer(Modifier.width(24.dp)) - } - } -} - -@Composable -private fun ListFilterTitle( - text: String, - active: Boolean, - itemsSelected: Int, - onClearAll: () -> Unit, - onSelectAll: () -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - FilterTitleText( - text = text, - active = active, - inactiveColor = Red - ) - - Spacer(Modifier.weight(1f)) - - Text( - modifier = Modifier - .clickable { - if (itemsSelected > 0) { - onClearAll() - } else { - onSelectAll() - } - } - .padding(all = 4.dp), //expand click area - text = if (itemsSelected > 0) stringResource(R.string.clear_all) else stringResource(R.string.select_all), - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - color = Color.Gray - ) - ) - - Spacer(modifier = Modifier.width(32.dp)) - } -} - -@Composable -private fun AmountFilter( - baseCurrency: String, - filter: ReportFilter?, - - onShowMinAmountModal: () -> Unit, - onShowMaxAmountModal: () -> Unit, -) { - FilterTitleText( - text = stringResource(R.string.amount_optional), - active = filter?.minAmount != null || filter?.maxAmount != null - ) - - Spacer(Modifier.height(16.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.width(32.dp)) - - Column( - modifier = Modifier.clickable { - onShowMinAmountModal() - }, - horizontalAlignment = Alignment.Start - ) { - Text( - text = stringResource(R.string.from), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - AmountCurrencyB1Row( - amount = filter?.minAmount ?: 0.0, - currency = baseCurrency - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - Column( - modifier = Modifier.clickable { - onShowMaxAmountModal() - }, - horizontalAlignment = Alignment.End - ) { - Text( - text = stringResource(R.string.to), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - AmountCurrencyB1Row( - amount = filter?.maxAmount ?: 0.0, - currency = baseCurrency - ) - } - Spacer(modifier = Modifier.width(32.dp)) - } -} - -@Composable -private fun KeywordsFilter( - filter: ReportFilter?, - nonNullFilter: (ReportFilter?) -> ReportFilter, - onSetFilter: (ReportFilter) -> Unit, - - onShowIncludeKeywordModal: () -> Unit, - onShowExcludeKeywordModal: () -> Unit, -) { - FilterTitleText( - text = stringResource(R.string.keywords_optional), - active = filter != null && - (filter.includeKeywords.isNotEmpty() || filter.excludeKeywords.isNotEmpty()) - ) - - Spacer(Modifier.height(12.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.includes_uppercase), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(12.dp)) - - val includes = nonNullFilter(filter).includeKeywords + listOf(AddKeywordButton()) - WrapContentRow( - modifier = Modifier.padding(horizontal = 24.dp), - items = includes - ) { item -> - when (item) { - is String -> { - Keyword( - keyword = item, - borderColor = UI.colorsInverted.pure - ) { - //Remove keyword - onSetFilter( - nonNullFilter(filter).copy( - includeKeywords = nonNullFilter(filter) - .includeKeywords.filter { it != item } - ) - ) - } - } - is AddKeywordButton -> { - AddKeywordButton(text = stringResource(R.string.add_keyword)) { - onShowIncludeKeywordModal() - } - } - } - } - - Spacer(Modifier.height(20.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.excludes_uppercase), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(12.dp)) - - val excludes = nonNullFilter(filter).excludeKeywords + listOf(AddKeywordButton()) - WrapContentRow( - modifier = Modifier.padding(horizontal = 24.dp), - items = excludes - ) { item -> - when (item) { - is String -> { - Keyword( - keyword = item, - borderColor = UI.colorsInverted.pure - ) { - //Remove keyword - onSetFilter( - nonNullFilter(filter).copy( - excludeKeywords = nonNullFilter(filter) - .excludeKeywords.filter { it != item } - ) - ) - } - } - is AddKeywordButton -> { - AddKeywordButton(text = stringResource(R.string.add_keyword)) { - onShowExcludeKeywordModal() - } - } - } - } - -} - -@Composable -private fun Keyword( - keyword: String, - borderColor: Color, - onClick: () -> Unit, -) { - IvyOutlinedButton( - text = keyword, - iconStart = R.drawable.ic_remove, - iconTint = Red, - borderColor = borderColor, - padding = 10.dp, - ) { - onClick() - } -} - -@Composable -private fun AddKeywordButton( - text: String, - onCLick: () -> Unit -) { - IvyOutlinedButton( - text = text, - iconStart = R.drawable.ic_plus, - padding = 10.dp, - ) { - onCLick() - } -} - -private class AddKeywordButton - - -@Composable -private fun FilterDivider() { - Spacer(modifier = Modifier.height(24.dp)) - - IvyDividerLine( - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(24.dp)) -} - -@Composable -private fun FilterTitleText( - text: String, - active: Boolean, - inactiveColor: Color = Color.Gray -) { - Text( - modifier = Modifier.padding(start = 32.dp), - text = text, - style = UI.typo.b1.style( - fontWeight = FontWeight.Medium, - color = if (active) UI.colorsInverted.pure else inactiveColor - ) - ) -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - val acc1 = AccountOld("Cash", color = Green.toArgb()) - val acc2 = AccountOld("DSK", color = GreenDark.toArgb()) - val cat1 = CategoryOld("Science", color = Purple1Dark.toArgb(), icon = "atom") - - FilterOverlay( - visible = true, - - baseCurrency = "BGN", - accounts = listOf( - acc1, - acc2, - AccountOld("phyre", color = GreenLight.toArgb(), icon = "cash"), - AccountOld("Revolut", color = IvyDark.toArgb()), - ), - categories = listOf( - cat1, - CategoryOld("Pet", color = Red3Light.toArgb(), icon = "pet"), - CategoryOld("Home", color = Green.toArgb(), icon = null), - ), - - filter = ReportFilter.emptyFilter("BGN").copy( - accounts = listOf( - acc1, acc2 - ), - categories = listOf( - cat1 - ), - minAmount = null, - maxAmount = 13256.27, - ), - onClose = { }, - onSetFilter = { - } - ) - } -} \ No newline at end of file diff --git a/reports/src/main/java/com/ivy/reports/ReportFilter.kt b/reports/src/main/java/com/ivy/reports/ReportFilter.kt deleted file mode 100644 index fc4c540ac7..0000000000 --- a/reports/src/main/java/com/ivy/reports/ReportFilter.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.ivy.reports - -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TrnTypeOld -import java.util.* - -data class ReportFilter( - val id: UUID = UUID.randomUUID(), - val trnTypes: List, - val period: TimePeriod?, - val accounts: List, - val categories: List, - val currency: String, - val minAmount: Double?, - val maxAmount: Double?, - val includeKeywords: List, - val excludeKeywords: List -) { - companion object { - fun emptyFilter( - baseCurrency: String - ) = ReportFilter( - trnTypes = emptyList(), - period = null, - accounts = emptyList(), - categories = emptyList(), - currency = baseCurrency, - includeKeywords = emptyList(), - excludeKeywords = emptyList(), - minAmount = null, - maxAmount = null - ) - } - - fun validate(): Boolean { - if (trnTypes.isEmpty()) return false - - if (period == null) return false - - if (accounts.isEmpty()) return false - - if (categories.isEmpty()) return false - - if (minAmount != null && maxAmount != null) { - if (minAmount > maxAmount) return false - if (maxAmount < minAmount) return false - } - - return true - } -} \ No newline at end of file diff --git a/reports/src/main/java/com/ivy/reports/ReportScreen.kt b/reports/src/main/java/com/ivy/reports/ReportScreen.kt deleted file mode 100644 index eda4a03178..0000000000 --- a/reports/src/main/java/com/ivy/reports/ReportScreen.kt +++ /dev/null @@ -1,435 +0,0 @@ -package com.ivy.reports - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.R -import com.ivy.base.data.AppBaseData -import com.ivy.base.data.DueSection -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.pure.IncomeExpensePair -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - - -import com.ivy.old.IncomeExpensesCards -import com.ivy.wallet.ui.component.transaction.TransactionsDividerLine -import com.ivy.wallet.ui.component.transaction.transactions -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.* -import com.ivy.wallet.utils.clickableNoIndication - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.ReportScreen() { - val viewModel: ReportViewModel = hiltViewModel() - val state by viewModel.state().collectAsState() - - UI( - state = state, - onEventHandler = viewModel::onEvent - ) -} - -@ExperimentalFoundationApi -@Composable -private fun BoxWithConstraintsScope.UI( - state: ReportScreenState = ReportScreenState(), - onEventHandler: (ReportScreenEvent) -> Unit = {} -) { - - val listState = rememberLazyListState() - val context = LocalContext.current - - if (state.loading) { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1000f) - .background(pureBlur()) - .clickableNoIndication { - //consume clicks - }, - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(R.string.generating_report), - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold, - color = Orange - ) - ) - } - } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - ) { - stickyHeader { - Toolbar( - onExport = { - onEventHandler.invoke(ReportScreenEvent.OnExport(context = context)) - }, - onFilter = { - onEventHandler.invoke( - ReportScreenEvent.OnFilterOverlayVisible( - filterOverlayVisible = true - ) - ) - } - ) - } - - item { - Text( - modifier = Modifier.padding( - start = 32.dp - ), - text = stringResource(R.string.reports), - style = UI.typo.h2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(8.dp)) - - BalanceRow( - modifier = Modifier - .padding(start = 32.dp), - textColor = UI.colorsInverted.pure, - currency = state.baseCurrency, - balance = state.balance, - balanceAmountPrefix = when { - state.balance > 0 -> "+" - else -> null - } - ) - - Spacer(Modifier.height(20.dp)) - - IncomeExpensesCards( - history = state.history, - currency = state.baseCurrency, - income = state.income, - expenses = state.expenses, - hasAddButtons = false, - itemColor = UI.colors.pure, - incomeHeaderCardClicked = { -// if (state.transactions.isNotEmpty()) -// nav.navigateTo( -// PieChartStatistic( -// type = TrnTypeOld.INCOME, -// transactions = state.transactions, -// accountList = state.accountIdFilters, -// treatTransfersAsIncomeExpense = state.treatTransfersAsIncExp -// ) -// ) - }, - expenseHeaderCardClicked = { -// if (state.transactions.isNotEmpty()) -// nav.navigateTo( -// PieChartStatistic( -// type = TrnTypeOld.EXPENSE, -// transactions = state.transactions, -// accountList = state.accountIdFilters, -// treatTransfersAsIncomeExpense = state.treatTransfersAsIncExp -// ) -// ) - } - ) - - if (state.showTransfersAsIncExpCheckbox) { - IvyCheckboxWithText( - modifier = Modifier - .padding(16.dp), - text = stringResource(R.string.transfers_as_income_expense), - checked = state.treatTransfersAsIncExp - ) { - onEventHandler.invoke( - ReportScreenEvent.OnTreatTransfersAsIncomeExpense( - transfersAsIncomeExpense = it - ) - ) - } - } else - Spacer(Modifier.height(32.dp)) - - TransactionsDividerLine( - paddingHorizontal = 0.dp - ) - - Spacer(Modifier.height(4.dp)) - } - - if (state.filter != null) { - transactions( - baseData = AppBaseData( - baseCurrency = state.baseCurrency, - categories = state.categories, - accounts = state.accounts, - ), - - upcoming = DueSection( - trns = state.upcomingTransactions, - stats = IncomeExpensePair( - income = state.upcomingIncome.toBigDecimal(), - expense = state.upcomingExpenses.toBigDecimal() - ), - expanded = state.upcomingExpanded - ), - - setUpcomingExpanded = { - onEventHandler.invoke(ReportScreenEvent.OnUpcomingExpanded(upcomingExpanded = it)) - }, - - overdue = DueSection( - trns = state.overdueTransactions, - stats = IncomeExpensePair( - income = state.overdueIncome.toBigDecimal(), - expense = state.overdueExpenses.toBigDecimal() - ), - expanded = state.overdueExpanded - ), - setOverdueExpanded = { - onEventHandler.invoke(ReportScreenEvent.OnOverdueExpanded(overdueExpanded = it)) - }, - - history = state.history, - lastItemSpacer = 48.dp, - - onPayOrGet = { - onEventHandler.invoke(ReportScreenEvent.OnPayOrGet(transaction = it)) - }, - emptyStateTitle = com.ivy.core.ui.temp.stringRes(R.string.no_transactions), - emptyStateText = com.ivy.core.ui.temp.stringRes(R.string.no_transactions_for_your_filter) - ) - } else { - item { - NoFilterEmptyState( - setFilterOverlayVisible = { - onEventHandler.invoke( - ReportScreenEvent.OnFilterOverlayVisible( - filterOverlayVisible = it - ) - ) - } - ) - } - } - } - - FilterOverlay( - visible = state.filterOverlayVisible, - baseCurrency = state.baseCurrency, - accounts = state.accounts, - categories = state.categories, - filter = state.filter, - onClose = { - onEventHandler.invoke( - ReportScreenEvent.OnFilterOverlayVisible( - filterOverlayVisible = false - ) - ) - }, - onSetFilter = { - onEventHandler.invoke(ReportScreenEvent.OnFilter(filter = it)) - } - ) -} - -@Composable -private fun NoFilterEmptyState( - setFilterOverlayVisible: (Boolean) -> Unit -) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.height(16.dp)) - - IvyIcon( - icon = R.drawable.ic_filter_l, - tint = Gray - ) - - Spacer(Modifier.height(8.dp)) - - Text( - text = stringResource(R.string.no_filter), - style = UI.typo.b1.style( - color = Gray, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.invalid_filter_warning), - style = UI.typo.b2.style( - color = Gray, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center - ) - ) - - Spacer(Modifier.height(32.dp)) - - IvyButton( - iconStart = R.drawable.ic_filter_xs, - text = stringResource(R.string.set_filter) - ) { - setFilterOverlayVisible(true) - } - - Spacer(Modifier.height(96.dp)) - } -} - -@Composable -private fun Toolbar( - onExport: () -> Unit, - onFilter: () -> Unit -) { - - IvyToolbar( - backButtonType = BackButtonType.CLOSE, - onBack = { - - } - ) { - Spacer(Modifier.weight(1f)) - - //Export CSV - IvyOutlinedButton( - text = stringResource(R.string.export), - iconTint = Green, - textColor = Green, - solidBackground = true, - padding = 8.dp, - iconStart = R.drawable.ic_export_csv - ) { - onExport() - } - - Spacer(Modifier.width(16.dp)) - - //Filter - CircleButtonFilled( - icon = R.drawable.ic_filter_xs - ) { - onFilter() - } - - Spacer(Modifier.width(24.dp)) - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview() { - IvyPreview { - val acc1 = AccountOld("Cash", color = Green.toArgb()) - val acc2 = AccountOld("DSK", color = GreenDark.toArgb()) - val cat1 = CategoryOld("Science", color = Purple1Dark.toArgb(), icon = "atom") - val state = ReportScreenState( - baseCurrency = "BGN", - balance = -6405.66, - income = 2000.0, - expenses = 8405.66, - upcomingIncome = 4800.23, - upcomingExpenses = 0.0, - overdueIncome = 2335.12, - overdueExpenses = 0.0, - history = emptyList(), - upcomingTransactions = emptyList(), - overdueTransactions = emptyList(), - - upcomingExpanded = true, - overdueExpanded = true, - filter = ReportFilter.emptyFilter("BGN"), - loading = false, - accounts = listOf( - acc1, - acc2, - AccountOld("phyre", color = GreenLight.toArgb(), icon = "cash"), - AccountOld("Revolut", color = IvyDark.toArgb()), - ), - categories = listOf( - cat1, - CategoryOld("Pet", color = Red3Light.toArgb(), icon = "pet"), - CategoryOld("Home", color = Green.toArgb(), icon = null), - ), - ) - - UI(state = state) - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview_NO_FILTER() { - IvyPreview() { - val acc1 = AccountOld("Cash", color = Green.toArgb()) - val acc2 = AccountOld("DSK", color = GreenDark.toArgb()) - val cat1 = CategoryOld("Science", color = Purple1Dark.toArgb(), icon = "atom") - val state = ReportScreenState( - baseCurrency = "BGN", - balance = 0.0, - income = 0.0, - expenses = 0.0, - upcomingIncome = 0.0, - upcomingExpenses = 0.0, - overdueIncome = 0.0, - overdueExpenses = 0.0, - - history = emptyList(), - upcomingTransactions = emptyList(), - overdueTransactions = emptyList(), - - upcomingExpanded = true, - overdueExpanded = true, - - filter = null, - loading = false, - - accounts = listOf( - acc1, - acc2, - AccountOld("phyre", color = GreenLight.toArgb(), icon = "cash"), - AccountOld("Revolut", color = IvyDark.toArgb()), - ), - categories = listOf( - cat1, - CategoryOld("Pet", color = Red3Light.toArgb(), icon = "pet"), - CategoryOld("Home", color = Green.toArgb(), icon = null), - ), - ) - - UI(state = state) - } -} \ No newline at end of file diff --git a/reports/src/main/java/com/ivy/reports/ReportScreenEvent.kt b/reports/src/main/java/com/ivy/reports/ReportScreenEvent.kt deleted file mode 100644 index 7ec1bdf3d1..0000000000 --- a/reports/src/main/java/com/ivy/reports/ReportScreenEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.ivy.reports - -import android.content.Context -import com.ivy.data.transaction.TransactionOld - -sealed class ReportScreenEvent { - data class OnFilter(val filter: ReportFilter?) : ReportScreenEvent() - data class OnExport(val context: Context) : ReportScreenEvent() - data class OnPayOrGet(val transaction: TransactionOld) : ReportScreenEvent() - data class OnUpcomingExpanded(val upcomingExpanded: Boolean) : ReportScreenEvent() - data class OnOverdueExpanded(val overdueExpanded: Boolean) : ReportScreenEvent() - data class OnFilterOverlayVisible(val filterOverlayVisible: Boolean) : ReportScreenEvent() - data class OnTreatTransfersAsIncomeExpense(val transfersAsIncomeExpense: Boolean) : - ReportScreenEvent() -} \ No newline at end of file diff --git a/reports/src/main/java/com/ivy/reports/ReportScreenState.kt b/reports/src/main/java/com/ivy/reports/ReportScreenState.kt deleted file mode 100644 index feb6726b26..0000000000 --- a/reports/src/main/java/com/ivy/reports/ReportScreenState.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.ivy.reports - -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld -import java.util.* - -data class ReportScreenState( - val baseCurrency: String = "", - val balance: Double = 0.0, - val income: Double = 0.0, - val expenses: Double = 0.0, - val upcomingIncome: Double = 0.0, - val upcomingExpenses: Double = 0.0, - val overdueIncome: Double = 0.0, - val overdueExpenses: Double = 0.0, - val history: List = emptyList(), - val upcomingTransactions: List = emptyList(), - val overdueTransactions: List = emptyList(), - val categories: List = emptyList(), - val accounts: List = emptyList(), - val upcomingExpanded: Boolean = false, - val overdueExpanded: Boolean = false, - val filter: ReportFilter? = null, - val loading: Boolean = false, - val accountIdFilters: List = emptyList(), - val transactions: List = emptyList(), - val filterOverlayVisible: Boolean = false, - val showTransfersAsIncExpCheckbox: Boolean = false, - val treatTransfersAsIncExp: Boolean = false -) \ No newline at end of file diff --git a/reports/src/main/java/com/ivy/reports/ReportViewModel.kt b/reports/src/main/java/com/ivy/reports/ReportViewModel.kt deleted file mode 100644 index ebb0d86261..0000000000 --- a/reports/src/main/java/com/ivy/reports/ReportViewModel.kt +++ /dev/null @@ -1,414 +0,0 @@ -package com.ivy.reports - -import android.content.Context -import androidx.compose.ui.graphics.toArgb -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.frp.filterSuspend - -import com.ivy.frp.viewmodel.FRPViewModel -import com.ivy.temp.persistence.ExchangeActOld -import com.ivy.temp.persistence.ExchangeData -import com.ivy.wallet.domain.action.account.AccountsActOld -import com.ivy.wallet.domain.action.category.CategoriesActOld -import com.ivy.wallet.domain.action.settings.BaseCurrencyActOld -import com.ivy.wallet.domain.action.transaction.CalcTrnsIncomeExpenseAct -import com.ivy.wallet.domain.action.transaction.TrnsWithDateDivsAct -import com.ivy.wallet.domain.deprecated.logic.csv.ExportCSVLogic -import com.ivy.wallet.domain.pure.data.IncomeExpenseTransferPair -import com.ivy.wallet.domain.pure.transaction.trnCurrency -import com.ivy.wallet.domain.pure.util.orZero -import com.ivy.wallet.io.persistence.dao.SettingsDao -import com.ivy.wallet.io.persistence.dao.TransactionDao -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.utils.* -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import java.math.BigDecimal -import javax.inject.Inject - -@HiltViewModel -class ReportViewModel @Inject constructor( - private val plannedPaymentsLogic: PlannedPaymentsLogic, - private val settingsDao: SettingsDao, - private val transactionDao: TransactionDao, - private val ivyContext: IvyWalletCtx, - private val - private val exportCSVLogic: ExportCSVLogic, - private val exchangeAct: ExchangeActOld, - private val accountsAct: AccountsActOld, - private val categoriesAct: CategoriesActOld, - private val trnsWithDateDivsAct: TrnsWithDateDivsAct, - private val calcTrnsIncomeExpenseAct: CalcTrnsIncomeExpenseAct, - private val baseCurrencyAct: BaseCurrencyActOld -) : FRPViewModel() { - override val _state: MutableStateFlow = MutableStateFlow( - ReportScreenState() - ) - - override suspend fun handleEvent(event: Nothing): suspend () -> ReportScreenState { - TODO("Not yet implemented") - } - - private val unSpecifiedCategory = - CategoryOld(com.ivy.core.ui.temp.stringRes(R.string.unspecified), color = Gray.toArgb()) - - private val _period = MutableLiveData() - val period = _period.asLiveData() - - private val _categories = MutableStateFlow>(emptyList()) - val categories = _categories.readOnly() - - private val _allAccounts = MutableStateFlow>(emptyList()) - - private val _baseCurrency = MutableStateFlow("") - val baseCurrency = _baseCurrency.readOnly() - - private val _historyIncomeExpense = MutableStateFlow(IncomeExpenseTransferPair.zero()) - private val historyIncomeExpense = _historyIncomeExpense.readOnly() - - private val _filter = MutableStateFlow(null) - val filter = _filter.readOnly() - - fun start() { - viewModelScope.launch(Dispatchers.IO) { - _baseCurrency.value = baseCurrencyAct(Unit) - _allAccounts.value = accountsAct(Unit) - _categories.value = listOf(unSpecifiedCategory) + categoriesAct(Unit) - - updateState { - it.copy( - baseCurrency = _baseCurrency.value, - categories = _categories.value, - accounts = _allAccounts.value - ) - } - } - } - - private suspend fun setFilter(filter: ReportFilter?) { - scopedIOThread { scope -> - if (filter == null) { - //clear filter - _filter.value = null - return@scopedIOThread - } - - if (!filter.validate()) return@scopedIOThread - val accounts = filter.accounts - val baseCurrency = baseCurrency.value - _filter.value = filter - - updateState { - it.copy(loading = true, filter = _filter.value) - } - - val transactions = filterTransactions( - baseCurrency = baseCurrency, - accounts = accounts, - filter = filter - ) - - val history = transactions - .filter { it.dateTime != null } - .sortedByDescending { it.dateTime } - - val historyWithDateDividers = scope.async { - trnsWithDateDivsAct( - TrnsWithDateDivsAct.Input( - baseCurrency = stateVal().baseCurrency, - transactions = history - ) - ) - } - - _historyIncomeExpense.value = calcTrnsIncomeExpenseAct( - CalcTrnsIncomeExpenseAct.Input( - transactions = history, - accounts = accounts, - baseCurrency = baseCurrency - ) - ) - - val income = historyIncomeExpense.value.income.toDouble() + - if (stateVal().treatTransfersAsIncExp) historyIncomeExpense.value.transferIncome.toDouble() else 0.0 - - val expenses = historyIncomeExpense.value.expense.toDouble() + - if (stateVal().treatTransfersAsIncExp) historyIncomeExpense.value.transferExpense.toDouble() else 0.0 - - val balance = calculateBalance(historyIncomeExpense.value).toDouble() - - val accountFilterIdList = scope.async { filter.accounts.map { it.id } } - - val timeNowUTC = timeNowUTC() - - //Upcoming - val upcomingTransactions = transactions - .filter { - it.dueDate != null && it.dueDate!!.isAfter(timeNowUTC) - } - .sortedBy { it.dueDate } - - val upcomingIncomeExpense = calcTrnsIncomeExpenseAct( - CalcTrnsIncomeExpenseAct.Input( - transactions = upcomingTransactions, - accounts = accounts, - baseCurrency = baseCurrency - ) - ) - //Overdue - val overdue = transactions.filter { - it.dueDate != null && it.dueDate!!.isBefore(timeNowUTC) - }.sortedByDescending { - it.dueDate - } - val overdueIncomeExpense = calcTrnsIncomeExpenseAct( - CalcTrnsIncomeExpenseAct.Input( - transactions = overdue, - accounts = accounts, - baseCurrency = baseCurrency - ) - ) - - updateState { - it.copy( - income = income, - expenses = expenses, - upcomingIncome = upcomingIncomeExpense.income.toDouble(), - upcomingExpenses = upcomingIncomeExpense.expense.toDouble(), - overdueIncome = overdueIncomeExpense.income.toDouble(), - overdueExpenses = overdueIncomeExpense.expense.toDouble(), - history = historyWithDateDividers.await(), - upcomingTransactions = upcomingTransactions, - overdueTransactions = overdue, - categories = categories.value, - accounts = _allAccounts.value, - filter = filter, - loading = false, - accountIdFilters = accountFilterIdList.await(), - transactions = transactions, - balance = balance, - filterOverlayVisible = false, - showTransfersAsIncExpCheckbox = filter.trnTypes.contains(TrnTypeOld.TRANSFER) - ) - } - } - } - - private suspend fun filterTransactions( - baseCurrency: String, - accounts: List, - filter: ReportFilter, - ): List { - val filterAccountIds = filter.accounts.map { it.id } - val filterCategoryIds = - filter.categories.map { if (it.id == unSpecifiedCategory.id) null else it.id } - val filterRange = filter.period?.toRange(ivyContext.startDayOfMonth) - - return transactionDao - .findAll() - .map { it.toDomain() } - .filter { - //Filter by Transaction Type - filter.trnTypes.contains(it.type) - } - .filter { - //Filter by Time Period - - filterRange ?: return@filter false - - (it.dateTime != null && filterRange.includes(it.dateTime!!)) || - (it.dueDate != null && filterRange.includes(it.dueDate!!)) - } - .filter { trn -> - //Filter by Accounts - - filterAccountIds.contains(trn.accountId) || //Transfers Out - (trn.toAccountId != null && filterAccountIds.contains(trn.toAccountId)) //Transfers In - } - .filter { trn -> - //Filter by Categories - - filterCategoryIds.contains(trn.categoryId) || (trn.type == TrnTypeOld.TRANSFER) - } - .filterSuspend { - //Filter by Amount - //!NOTE: Amount must be converted to baseCurrency amount - - val trnAmountBaseCurrency = exchangeAct( - ExchangeActOld.Input( - data = ExchangeData( - baseCurrency = baseCurrency, - fromCurrency = trnCurrency(it, accounts, baseCurrency), - ), - amount = it.amount - ) - ).orZero().toDouble() - - (filter.minAmount == null || trnAmountBaseCurrency >= filter.minAmount) && - (filter.maxAmount == null || trnAmountBaseCurrency <= filter.maxAmount) - } - .filter { - //Filter by Included Keywords - - val includeKeywords = filter.includeKeywords - if (includeKeywords.isEmpty()) return@filter true - - if (it.title != null && it.title!!.isNotEmpty()) { - includeKeywords.forEach { keyword -> - if (it.title!!.containsLowercase(keyword)) { - return@filter true - } - } - } - - if (it.description != null && it.description!!.isNotEmpty()) { - includeKeywords.forEach { keyword -> - if (it.description!!.containsLowercase(keyword)) { - return@filter true - } - } - } - - false - } - .filter { - //Filter by Excluded Keywords - - val excludedKeywords = filter.excludeKeywords - if (excludedKeywords.isEmpty()) return@filter true - - if (it.title != null && it.title!!.isNotEmpty()) { - excludedKeywords.forEach { keyword -> - if (it.title!!.containsLowercase(keyword)) { - return@filter false - } - } - } - - if (it.description != null && it.description!!.isNotEmpty()) { - excludedKeywords.forEach { keyword -> - if (it.description!!.containsLowercase(keyword)) { - return@filter false - } - } - } - - true - } - .toList() - } - - private fun String.containsLowercase(anotherString: String): Boolean { - return this.toLowerCaseLocal().contains(anotherString.toLowerCaseLocal()) - } - - private fun calculateBalance(incomeExpenseTransferPair: IncomeExpenseTransferPair): BigDecimal { - return incomeExpenseTransferPair.income + incomeExpenseTransferPair.transferIncome - incomeExpenseTransferPair.expense - incomeExpenseTransferPair.transferExpense - } - - private suspend fun export(context: Context) { - val filter = _filter.value ?: return - if (!filter.validate()) return - val accounts = _allAccounts.value - val baseCurrency = _baseCurrency.value - - ivyContext.createNewFile( - "Report (${ - timeNowUTC().formatNicelyWithTime(noWeekDay = true) - }).csv" - ) { fileUri -> - viewModelScope.launch { - updateState { - it.copy(loading = true) - } - - exportCSVLogic.exportToFile( - context = context, - fileUri = fileUri, - exportScope = { - filterTransactions( - baseCurrency = baseCurrency, - accounts = accounts, - filter = filter - ) - } - ) - - (context as com.ivy.core.ui.temp.RootScreen).shareCSVFile( - fileUri = fileUri - ) - - updateState { - it.copy(loading = false) - } - } - } - } - - private fun setUpcomingExpanded(expanded: Boolean) { - updateStateNonBlocking { - it.copy(upcomingExpanded = expanded) - } - } - - private fun setOverdueExpanded(expanded: Boolean) { - updateStateNonBlocking { - it.copy(overdueExpanded = expanded) - } - } - - private suspend fun payOrGet(transaction: TransactionOld) { - uiThread { - plannedPaymentsLogic.payOrGet(transaction = transaction) { - start() - } - } - } - - private fun setFilterOverlayVisible(filterOverlayVisible: Boolean) { - updateStateNonBlocking { - it.copy(filterOverlayVisible = filterOverlayVisible) - } - } - - private suspend fun onTreatTransfersAsIncomeExpense(treatTransfersAsIncExp: Boolean) { - updateState { - val income = historyIncomeExpense.value.income.toDouble() + - if (treatTransfersAsIncExp) historyIncomeExpense.value.transferIncome.toDouble() else 0.0 - val expenses = historyIncomeExpense.value.expense.toDouble() + - if (treatTransfersAsIncExp) historyIncomeExpense.value.transferExpense.toDouble() else 0.0 - it.copy( - treatTransfersAsIncExp = treatTransfersAsIncExp, - income = income, - expenses = expenses - ) - } - } - - fun onEvent(event: ReportScreenEvent) { - viewModelScope.launch(Dispatchers.Default) { - when (event) { - is ReportScreenEvent.OnFilter -> setFilter(event.filter) - is ReportScreenEvent.OnExport -> export(event.context) - is ReportScreenEvent.OnPayOrGet -> payOrGet(event.transaction) - is ReportScreenEvent.OnOverdueExpanded -> setOverdueExpanded(event.overdueExpanded) - is ReportScreenEvent.OnUpcomingExpanded -> setUpcomingExpanded(event.upcomingExpanded) - is ReportScreenEvent.OnFilterOverlayVisible -> setFilterOverlayVisible(event.filterOverlayVisible) - is ReportScreenEvent.OnTreatTransfersAsIncomeExpense -> onTreatTransfersAsIncomeExpense( - event.transfersAsIncomeExpense - ) - } - } - } -} \ No newline at end of file diff --git a/resources/src/main/res/drawable/outline_backspace_24.xml b/resources/src/main/res/drawable/outline_backspace_24.xml new file mode 100644 index 0000000000..9ebe935e71 --- /dev/null +++ b/resources/src/main/res/drawable/outline_backspace_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/resources/src/main/res/drawable/outline_delete_24.xml b/resources/src/main/res/drawable/outline_delete_24.xml new file mode 100644 index 0000000000..9196eea587 --- /dev/null +++ b/resources/src/main/res/drawable/outline_delete_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/outline_info_24.xml b/resources/src/main/res/drawable/outline_info_24.xml new file mode 100644 index 0000000000..caacb830e0 --- /dev/null +++ b/resources/src/main/res/drawable/outline_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_archive_24.xml b/resources/src/main/res/drawable/round_archive_24.xml new file mode 100644 index 0000000000..5c29e42f7a --- /dev/null +++ b/resources/src/main/res/drawable/round_archive_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_arrow_back_ios_24.xml b/resources/src/main/res/drawable/round_arrow_back_ios_24.xml new file mode 100644 index 0000000000..2dbf9d0224 --- /dev/null +++ b/resources/src/main/res/drawable/round_arrow_back_ios_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/resources/src/main/res/drawable/round_currency_exchange_24.xml b/resources/src/main/res/drawable/round_currency_exchange_24.xml new file mode 100644 index 0000000000..5be5fa1502 --- /dev/null +++ b/resources/src/main/res/drawable/round_currency_exchange_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_done_24.xml b/resources/src/main/res/drawable/round_done_24.xml new file mode 100644 index 0000000000..e620ad8ec8 --- /dev/null +++ b/resources/src/main/res/drawable/round_done_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_expand_more_24.xml b/resources/src/main/res/drawable/round_expand_more_24.xml new file mode 100644 index 0000000000..989203bd79 --- /dev/null +++ b/resources/src/main/res/drawable/round_expand_more_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_remove_24.xml b/resources/src/main/res/drawable/round_remove_24.xml new file mode 100644 index 0000000000..7d2baa8428 --- /dev/null +++ b/resources/src/main/res/drawable/round_remove_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_reorder_24.xml b/resources/src/main/res/drawable/round_reorder_24.xml new file mode 100644 index 0000000000..f077e12158 --- /dev/null +++ b/resources/src/main/res/drawable/round_reorder_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_time_24.xml b/resources/src/main/res/drawable/round_time_24.xml new file mode 100644 index 0000000000..f71c61b43a --- /dev/null +++ b/resources/src/main/res/drawable/round_time_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/drawable/round_unarchive_24.xml b/resources/src/main/res/drawable/round_unarchive_24.xml new file mode 100644 index 0000000000..9a5ea5d747 --- /dev/null +++ b/resources/src/main/res/drawable/round_unarchive_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/resources/src/main/res/values-pt/strings.xml b/resources/src/main/res/values-pt/strings.xml index ad92861165..1d72792a90 100644 --- a/resources/src/main/res/values-pt/strings.xml +++ b/resources/src/main/res/values-pt/strings.xml @@ -108,6 +108,7 @@ Empréstimos Definir moeda Nenhuma transação + Você não tem transações para o período selecionado. Você não tem transações para %1$s.\nVocê pode adicionar uma tocando no botão \"+\". Adicionar empréstimo Sem empréstimos @@ -295,10 +296,11 @@ Escolha o mês ou intervalo personalizado Adicionar data - ou no último - ou todos os tempos - Desmarcar todos os tempos - Selecionar todos os tempos + ou no período + ou todo o período + Desmarcar todo o período + Selecionar todo o período + Todo o período Escolha a data de início do mês suporta criptografia Excluir @@ -428,13 +430,23 @@ Início Gerando relatório… " Classificar por" - Puxar tudo + Pular tudo Confirmar pular tudo - Tem certeza de que pula todas as transações planejadas expiradas? + Tem certeza de que deseja pular todas as transações planejadas expiradas? Mudar para o modo offline AVISO! Esta ação excluirá todos os seus dados armazenados na nuvem por %1$s PERMANENTEMENTE, os dados offline armazenados em seu aplicativo local permanecerão. Excluir todos os dados armazenados na nuvem? Experimental Configurações experimentais Saldo da carteira + Marcar como subcategoria + Categoria principal + *Marcada como uma categoria pai + Descompactar todas as subcategorias + Marcar todos \"Optional columns\" nas opções de exportação + Renomear categoria de transferência + Para usuários não ingleses, renomeie a categoria do sistema de transferência para \"Transfer\" no arquivo de exportação + Ressalvas + As subcategorias não são suportadas + As colunas Evento, Pessoas e Local serão ignoradas diff --git a/resources/src/main/res/values-uk/strings.xml b/resources/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..54db2d63a4 --- /dev/null +++ b/resources/src/main/res/values-uk/strings.xml @@ -0,0 +1,452 @@ + + + Рахунки + Всього: %1$s %2$s + ДОХІД ЗА МІСЯЦЬ + ВИТРАТИ ЗА МІСЯЦЬ + (виключено) + ДОХОДИ + ВИТРАТИ + ДОДАТОК ЗАБЛОКОВАНО + Авторизуйтесь, щоб увійти в застосунок + Розблокувати + ПОТОЧНИЙ БАЛАНС + БАЛАНС ПІСЛЯ ЗАПЛАНОВИХ ПЛАТЕЖІВ + Підключитися + Синхронізувати транзакції + Синхронізація транзакцій… + Синхронізація з банком ввімкнена: + Видалити клієнта + Додати бюджет + Бюджети відсутні + У вас не встановлено жодного бюджету.\nНатисніть \"+ Додати бюджет\", щоб додати його. + Бюджети + %1$s %2$s для категорій + %1$s %2$s бюджет застосунку + Інформація про бюджет: %1$s / %2$s + Інформація про бюджет: %1$s%2$s + Додати категорію + Витрати + Кількість витрат + Доходи + Кількість доходів + Діаграма балансу + БАЛАНС %1$s + Діаграми + Період: + Категорії + Експорт CSV файлу + Експорт CSV файлу зі стандартними параметрами + Використовуйте стандартні параметри та обов\'язково додайте заголовки. + Як імпортувати + відкрити + Кроки + Як + Відео + Інструкція + Завантажити CSV файл + Експорт даних + Завантажити CSV/ZIP файл + Експорт у файл + Кодування: UTF-8\nДесятковий роздільник: Десяткова точка \'.\'\nСимвол роздільника: Кома \',\' + Експорт Excel файлу + Конвертувати XLS в CSV + !ПРИМІТКА: Якщо експортований файл не має розширення \".xls\", додайте його, перейменувавши файл вручну. + Безкоштовний онлайн CSV конвертер + Перевірте папки \"Промоакції\" та \"Спам\" вашої електронної пошти. + Завантажте файл \"transactions_export…\" прикріплений до електронного листа. + Якщо у вас більше ніж одна валюта, вам доведеться завантажити кожен файл \"transactions_export…\" та імпортувати його в Ivy. + Імпортувати з + Будь ласка, зачекайте + Імпорт CSV файлу + Успіх + Виникла помилка + Завантажено + %1$d транзакцій + %1$d рахунків + %1$d категорій + Не вдалося + Не вдалось розпізнати %1$d рядків із CSV файлу + Закінчити + Додайте опис + Опис + Заплановано на + Додати гроші до + Оплатити з + З + Рахунок + ПО + Додати рахунок + Заголовок доходу + Заголовок витрати + Заголовок переказу + Витрата + Додати дату запланованого платежу + Заплатити + Отримати + Підтвердити видалення + Видалення цієї транзакції призведе до видалення її з історії транзакцій і відповідного оновлення балансу. + Підтвердити зміни в рахунку + Примітка: Ви намагаєтеся змінити обліковий запис, пов\'язаний з кредитом з рахунком в іншій валюті. \nВсі записи про кредит буде перераховано на основі поточних курсів валют + Підтвердити + Будь ласка, зачекайте, перераховуєм всі дані по кредиту + Створено + Привіт + Привіт, %1$s + Грошовий потік: %1$s%2$s %3$s + Пошук транзакції + Ivy Wallet має відкритий код + Ціль заощадження + Швидкий доступ + Налаштування + Світла тема + Темна тема + Системна тема + Заплановані\nплатежі + Поділись Ivy + Звіти + Кредити + Встановити валюту + Немає транзакцій + У вас немає транзакцій за вибраний період. + У вас немає жодної транзакції за %1$s.\nЩоб додати, натисніть кнопку \"+\" + Додати кредит + Немає кредитів + У вас немає жодного кредиту.\nЩоб додати, натисніть \"+ Додати кредит\". + Примітка: Видалення цього кредиту призведе до його остаточного видалення, включно з усіма записами, пов\'язаними з цим кредитом. + Будь ласка, зачекайте, перераховуються всіх записи по кредиту + Оплачено + %1$s %2$s залишилось + Відсотки по кредиту + %1$s %2$s оплачено + Додати запис + Відсотки + Немає записів + У вас немає жодних записів щодо цього кредиту. Щоб створити, натисніть \"Додати запис\". + Додати дохід + Додати витрату + Невизначений + %1$s\%% + Перекази + У вас немає транзакцій за %1$s.\nЩоб додати, натисніть кнопку \"Додати дохід\" або \"Додати витрату\", розташовану вище. + Примітка: Видалення цього облікового запису призведе до його остаточного видалення та видалення всіх пов\'язаних з ним транзакцій. + Примітка: Видалення цієї категорії призведе до її остаточного видалення. + Редагувати + транзакції + Головна + Додати запланований платіж + ДОДАТИ ДОХІД + ДОДАТИ ВИТРАТУ + ПЕРЕКАЗ + Пропустити + Додати нову + З %1$s + До %1$s + Діапазон + Конфіденційність і\nзбір даних + Проведіть пальцем, щоб прийняти наші умови використання + Погоджуюсь з умовами + Проведіть пальцем, щоб погодитися з нашою політикою конфіденційності + Погоджуюсь з політикою конфіденційності + Правила та умови + Політика конфіденційності + Відстежуйте свої доходи, витрати та бюджет за допомогою Ivy.\n\nІнтуїтивно зрозумілий інтерфейс, регулярні та заплановані платежі, керування кількома рахунками, упорядкування транзакцій за категоріями, змістовна статистика, експорт у CSV і багато іншого. + Введіть своє ім\'я,\nщоб персоналізувати свій\nгаманець + Як вас звати? + Ввести + Додати рахунки + Пропозиція + Далі + Додати категорії + Пропозиції + Встановити + Ваш персональний фінансовий менеджер + #opensource + Помилка. Спробуйте знову: %1$s + Вхід… + Успішно! + Увійти за допомогою Google + Офлайн акаунт + СИНХРОНІЗУЙТЕ СВОЇ ДАНІ З IVY CLOUD + Цілісність і захист даних не гарантуються! + АБО УВІЙТИ З ОФЛАЙН АКАУНТУ + Ваші дані будуть збережені локально (тільки на вашому телефоні) і не будуть синхронізовані з хмарою. Ви ризикуєте втратити їх, якщо видалите програму або зміните пристрій. Ви завжди можете активувати синхронізацію пізніше. + + Увійшовши, ви погоджуєтеся з %1$s та %2$s. + Завантажте CSV файл + з Ivy або іншого застосунку + Завантаження файлу резервної копії з іншого застосунку може тривати до 5 хвилин. Ви завжди можете імпортувати свої дані пізніше. + Завантажити файл резервної копії + Почати спочатку + Видалення запланованого платежу призведе до видалення всіх майбутніх несплачених або прострочених транзакцій, пов\'язаних із ним. + Встановити тип оплати + Запланувати початок на + ПОВТОРЮЄТЬСЯ КОЖНІ %1$d %2$s + видалено + \"ЗАПЛАНОВАНО НА \" + null + \"ПОЧИНАЄТЬСЯ З %1$s \" + Додати оплату + Одноразові платежі + Регулярні платежі + Немає планових платежів + У вас немає запланованих платежів.\nНатисніть кнопку \'⚡\' внизу, щоб додати. + Заплановані платежі + Сьогодні + Вчора + Завтра + Термін погашення %1$s + Майбутні + Прострочені + витрати + дохід + Редагувати рахунок + Новий рахунок + Назва рахунку + Включати рахунок + Введіть баланс рахунку + Виберіть валюту + Калькулятор + Розрахунок (+-/*=) + Редагувати категорію + Створити категорію + Назва категорії + Обрати категорію + Додати деталі + Очистити фільтр + Фільтр + Застосувати фільтр + За типом + Доходи + Період часу + Виберіть діапазон часу + Рахунки (%1$d) + Категорії (%1$d) + Очистити все + Вибрати все + Сума (необов\'язково) + Ключові слова (необов\'язково) + ВКЛЮЧАЄ + Додати ключове слово + ВИКЛЮЧЕННЯ + У вас немає транзакцій для вашого фільтра. + Без фільтра + Щоб створити звіт, спочатку встановіть фільтр. + Встановити фільтр + Експорт + У вас немає транзакцій по запиту: \"%1$s\". + + Резервне копіювання даних + Імпорт даних + Налаштування програми + Заблокувати програму + Показувати сповіщення + Приховати баланс + Натисніть на прихований баланс, щоб показати його на 5 секунд + Інші + Оцініть нас Google Play + Поділіться Ivy Wallet + Продукт + Небезпечна зона + Видалити всі дані користувача + Видалити всі дані користувача? + УВАГА! Ця дія НАЗАВЖДИ видалить всі дані для %1$s і ви не зможете їх відновити. + ваш обліковий запис + Підтвердити остаточне видалення для \'%1$s\' + всіх ваших даних + ОСТАННЕ ПОПЕРЕДЖЕННЯ! Після натискання \"Видалити\" ваші дані зникнуть назавжди. + Експорт даних + Будь ласка зачекайте, іде експорт даних + Початок місяця + Ivy Telegram + Центр допомоги + Roadmap + Запит функції + Зв\'язок з службою підтримки + Учасники проекту + ОБЛІКОВИЙ ЗАПИС + Вийти + Увійти + Синхронізація… + Дані синхронізовані з хмарою + Натисніть, щоб синхронізувати + Помилка синхронізації. Натисніть, щоб синхронізувати + Анонім + Експорт в CSV + Залишилось витратити + Бюджет перевищено на + Буфер перевищено на + Встановіть тип транзакції + Переказ + Вибраний + Пошук (USD, EUR, GBP, BTC і т.д.) + Попередньо обрана + Криптовалюта + Курс обміну + Виберіть колір + Змінити порядок + Ключове слово + Редагувати бюджет + Створити бюджет + Назва бюджету + СУМА БЮДЖЕТУ + Ви впевнені, що бажаєте видалити бюджет \"%1$s\"? + Редагувати ціль заощаджень + Виберіть значок + Виберіть місяць + або індивідуальний діапазон + Додати дату + або в останній + або за весь час + Скасувати \"За весь час\" + Вибрати \"За весь час\" + За весь час + Виберіть дату початку місяця + підтримує криптовалюти + Видалити + Зберегти + Додати + Створити + Редагувати кредит + Новий кредит + Назва кредиту + Пов\'язаний рахунок + Створити основну транзакцію + ВВЕДІТЬ СУМУ КРЕДИТУ + "Примітка: Ви намагаєтеся змінити рахунок, пов\'язаний з кредитом в іншій валюті. \nВсі кредитні записи будуть перераховані на основі поточного курсу валют " + Тип кредиту + Позичити гроші + Дати в борг гроші + Редагувати запис + Новий запис + Примітка + Позначити як \"Цікаво\" + Перерахувати суму за сьогоднішнім курсом обміну валют + ВВЕДІТЬ СУМУ ЗАПИСУ + Ви впевнені, що хочете видалити запис "%1$s"? + "Примітка: Ви намагаєтесь змінити рахунок, пов\'язаний із записом про кредит в іншій валюті.\nСума буде перерахована за поточним курсом валют " + Редагувати назву + Запланувати на + Один раз + Кілька разів + Починається з + Повторювати кожен + Надіслати + Що вам потрібно? + Поясніть це одним реченням. (підтримує markdown) + Останні 12 місяців + Останні 6 місяців + Останні 4 тижні + Останні 7 днів + Сьогодні, %1$s + Вчора, %1$s + Завтра, %1$s + Термін дії минув + Автентифікація пройшла успішно! + Помилка автентифікації. + Ви робили якісь транзакції сьогодні? 🏁 + Ви відслідковували свої витрати сьогодні? 💸 + Ви записали свої транзакції сьогодні? 🏁 + Готівка + Банк + Revolut + + + Транспорт + Продукти + Розваги + Покупки + Подарунки + Здоров\'я + Інвестиції + Автомобіль + Робота + Ресторан + Сім\'я + Соціальне життя + Доставка їжі + Подорожі + Фітнес + Саморозвиток + Одяг + Краса + Освіта + Домашні тварини + Спорт + Відрегулюйте початковий баланс + До рахунків + Натисніть на рахунок -> Натисніть на його баланс -> Введіть поточний баланс. Ось і все!]]> + Створіть свій перший запланований платіж + Автоматизуйте відстеження повторюваних транзакцій, таких як: підписки, орендна плата, зарплата тощо. Будьте попереду своїх фінансів, знаючи, скільки вам потрібно заплатити/отримати. + Ви знали? + Ivy Wallet має крутий віджет, який дозволяє додавати ДОХОДИ/ВИТРАТИ/ПЕРЕКАЗИ в один дотик зі стартового екрану вашого смартфона.\n\nПримітка: Якщо кнопка \"Додати віджет\" не працює, додайте його вручну з меню віджетів. + Додати віджет + Встановити бюджет + Ivy Wallet не тільки допомагає вам пасивно відстежувати свої витрати, але й активно створювати своє фінансове майбутнє, встановлюючи бюджети та дотримуючись їх. + Ви можете переглянути структуру своїх витрат за категоріями! Спробуйте, натисніть сіру/чорну кнопку \"Витрати\" прямо під вашим балансом. + Секторна кругова діаграма + Оцініть Ivy Wallet + Надішліть нам свій відгук! Допоможіть Ivy Wallet стати кращим і розвиватися, написавши нам відгук. Компліменти, ідеї та критика вітаються! Ми робимо все можливе.\n\nЗ найкращими побажаннями,\nКоманда Ivy + Допоможіть нам розвиватися, щоб ми могли інвестувати більше в розробку та робити додаток кращим для вас. Поділившись Ivy Wallet, ви порадуєте двох розробників, а також допоможете другові контролювати його фінанси. + Поділися з друзями + Ви можете створювати звіти, щоб отримати детальну інформацію про свої доходи та витрати. Фільтруйте свої транзакції за типом, періодом часу, категорією, рахунками, сумою, ключовими словами тощо, щоб отримати кращий огляд своїх фінансів. + Складіть звіт + Хочете зробити Ivy Wallet кращим? Напишіть нам відгук. Для нас це єдиний спосіб розробляти те, що ви хочете та потребуєте. Крім того, це допомагає нам займати вищі позиції в PlayStore, щоб ми могли витрачати гроші на продукт, а не на маркетинг.\n\nМи робимо все можливе.\nКоманда Ivy + Нам потрібна ваша допомога! + Ми звичайні дизайнер і розробник, які працюють над програмою після основної роботи. Зараз ми вкладаємо багато часу та грошей, а отримуємо лише витрати та виснаження. Якщо ви хочете, щоб ми продовжували позробку Ivy Wallet, поділіться ним із друзями та родиною.\n\nP.S. Відгуки Google PlayStore також дуже допомагають! + Ivy Wallet має відкритий код! + Ivy Wallet має відкритий код, і кожен може його побачити. Ми віримо, що прозорість і етика є обов\'язковими для кожного програмного продукту. Якщо вам подобається наша робота і ви хочете покращити програму, ви можете внести свій внесок у наш загальнодоступний репозиторій Github. + Внести свій внесок + Відрегулюйте баланс + Потрібна автентифікація + Доведіть, що у вас є доступ до цього пристрою, щоб розблокувати програму. + Загальний бюджет + Бюджет категорії + Багатокатегорійний (%1$s) бюджет + ПОЗИЧИЛИ + ДАЛИ В БОРГ + Січень + Лютий + Березень + Квітень + Травень + Червень + Липень + Серпень + Вересень + Жовтень + Листопад + Грудень + днів + день + тижні + тиждень + місяців + місяць + років + рік + + Розглядати перекази рахунків як дохід або витрату на екрані \"Рахунки\" + Дім + Створення звіту… + Сортувати за + Пропустити все + Підтвердити, пропустити все + Ви впевнені, що бажаєте пропустити всі прострочені планові платежі? + Перейти в автономний режим + УВАГА! Ця дія видалить всі ваші хмарні дані для %1$s НАЗАВЖДИ, офлайн-дані, збережені у вашій локальній програмі, залишаться. + Видалити всі дані, які зберігаються в хмарі? + Експериментальний + Експериментальні налаштування + Баланс гаманця + Позначити як підкатегорію + Батьківська категорія + *Це позначено як батьківську категорію + Розпакуйте всі підкатегорії + Перевірте всі \"Необов\'язкові стовпці\" в параметрах експорту + Перейменувати категорію переказу + Для користувачів, які не володіють англійською мовою, перейменувати системну категорію переказу на \"Переказ\" у файлі експорту + Застереження + Підкатегорії не підтримуються + Стовпці \"Подія\", \"Люди\" та \"Місце\" ігноруватимуться + \ No newline at end of file diff --git a/scripts/templates/build.gradle.kts.template b/scripts/templates/build.gradle.kts.template index 34f853f66c..1229a440e4 100644 --- a/scripts/templates/build.gradle.kts.template +++ b/scripts/templates/build.gradle.kts.template @@ -1,4 +1,5 @@ import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing apply() @@ -9,4 +10,5 @@ plugins { dependencies { Hilt() + Testing() } \ No newline at end of file diff --git a/search-transactions/.gitignore b/search-transactions/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/search-transactions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/search-transactions/README.md b/search-transactions/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/search-transactions/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/search-transactions/build.gradle.kts b/search-transactions/build.gradle.kts deleted file mode 100644 index 11ef563d20..0000000000 --- a/search-transactions/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) -} \ No newline at end of file diff --git a/search-transactions/src/main/AndroidManifest.xml b/search-transactions/src/main/AndroidManifest.xml deleted file mode 100644 index 9a076c32a7..0000000000 --- a/search-transactions/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/search-transactions/src/main/java/com/ivy/search/SearchScreen.kt b/search-transactions/src/main/java/com/ivy/search/SearchScreen.kt deleted file mode 100644 index 415a3ab15f..0000000000 --- a/search-transactions/src/main/java/com/ivy/search/SearchScreen.kt +++ /dev/null @@ -1,195 +0,0 @@ -package com.ivy.search - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.R -import com.ivy.base.data.AppBaseData -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.design.l0_system.UI -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.ui.component.transaction.transactions -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.IvyBasicTextField -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.modal.DURATION_MODAL_ANIM -import com.ivy.wallet.utils.densityScope -import com.ivy.wallet.utils.keyboardOnlyWindowInsets -import com.ivy.wallet.utils.keyboardVisibleState -import com.ivy.wallet.utils.selectEndTextFieldValue - -@Composable -fun SearchScreen() { - val viewModel: SearchViewModel = hiltViewModel() - - val transactions by viewModel.transactions.collectAsState() - val baseCurrency by viewModel.baseCurrencyCode.collectAsState() - val categories by viewModel.categories.collectAsState() - val accounts by viewModel.accounts.collectAsState() - - UI( - transactions = transactions, - baseCurrency = baseCurrency, - categories = categories, - accounts = accounts, - - onSearch = viewModel::search - ) -} - -@Composable -private fun UI( - transactions: List, - baseCurrency: String, - categories: List, - accounts: List, - - onSearch: (String) -> Unit = {} -) { - Column( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - ) { - Spacer(Modifier.height(24.dp)) - - val listState = rememberLazyListState() - - var searchQueryTextFieldValue by remember { - mutableStateOf(selectEndTextFieldValue("")) - } - - SearchInput( - searchQueryTextFieldValue = searchQueryTextFieldValue, - onSetSearchQueryTextField = { - searchQueryTextFieldValue = it - onSearch(it.text) - } - ) - - LaunchedEffect(transactions) { - //scroll to top when transactions are changed - listState.animateScrollToItem(index = 0, scrollOffset = 0) - } - - Spacer(Modifier.height(16.dp)) - - - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = listState - - ) { - transactions( - baseData = AppBaseData( - baseCurrency = baseCurrency, - accounts = accounts, - categories = categories - ), - upcoming = null, - setUpcomingExpanded = { }, - overdue = null, - setOverdueExpanded = { }, - history = transactions, - onPayOrGet = { }, - emptyStateTitle = com.ivy.core.ui.temp.stringRes(R.string.no_transactions), - emptyStateText = com.ivy.core.ui.temp.stringRes( - R.string.no_transactions_for_query, - searchQueryTextFieldValue.text - ), - dateDividerMarginTop = 16.dp - ) - - item { - val keyboardVisible by keyboardVisibleState() - val keyboardShownInsetDp by animateDpAsState( - targetValue = densityScope { - if (keyboardVisible) keyboardOnlyWindowInsets().bottom.toDp() else 0.dp - }, - animationSpec = tween(DURATION_MODAL_ANIM) - ) - - Spacer(Modifier.height(keyboardShownInsetDp)) - //add keyboard height margin at bototm so the list can scroll to bottom - } - } - } -} - -@Composable -private fun SearchInput( - searchQueryTextFieldValue: TextFieldValue, - onSetSearchQueryTextField: (TextFieldValue) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.fullyRounded) - .background(UI.colors.pure) - .border(1.dp, Gray, UI.shapes.fullyRounded), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - IvyIcon(icon = R.drawable.ic_search) - - Spacer(Modifier.width(12.dp)) - - val searchFocus = FocusRequester() - IvyBasicTextField( - modifier = Modifier - .padding(vertical = 12.dp) - .focusRequester(searchFocus), - value = searchQueryTextFieldValue, - hint = stringResource(R.string.search_transactions), - onValueChanged = { - onSetSearchQueryTextField(it) - } - ) - - Spacer(Modifier.weight(1f)) - - IvyIcon( - modifier = Modifier - .clickable { - onSetSearchQueryTextField(selectEndTextFieldValue("")) - } - .padding(all = 12.dp), //enlarge click area - icon = R.drawable.ic_outline_clear_24 - ) - - Spacer(Modifier.width(8.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - UI( - transactions = emptyList(), - baseCurrency = "BGN", - categories = emptyList(), - accounts = emptyList() - ) - } -} \ No newline at end of file diff --git a/search-transactions/src/main/java/com/ivy/search/SearchViewModel.kt b/search-transactions/src/main/java/com/ivy/search/SearchViewModel.kt deleted file mode 100644 index 6114077cde..0000000000 --- a/search-transactions/src/main/java/com/ivy/search/SearchViewModel.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.ivy.search - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.getDefaultFIATCurrency -import com.ivy.frp.test.TestIdlingResource -import com.ivy.wallet.domain.action.account.AccountsActOld -import com.ivy.wallet.domain.action.category.CategoriesActOld -import com.ivy.wallet.domain.action.settings.BaseCurrencyActOld -import com.ivy.wallet.domain.action.transaction.AllTrnsAct -import com.ivy.wallet.domain.action.transaction.TrnsWithDateDivsAct -import com.ivy.wallet.utils.ioThread -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SearchViewModel @Inject constructor( - private val trnsWithDateDivsAct: TrnsWithDateDivsAct, - private val accountsAct: AccountsActOld, - private val categoriesAct: CategoriesActOld, - private val baseCurrencyAct: BaseCurrencyActOld, - private val allTrnsAct: AllTrnsAct -) : ViewModel() { - - private val _baseCurrencyCode = MutableStateFlow(getDefaultFIATCurrency().currencyCode) - val baseCurrencyCode = _baseCurrencyCode.asStateFlow() - - private val _transactions = MutableStateFlow(emptyList()) - val transactions = _transactions.asStateFlow() - - private val _accounts = MutableStateFlow(emptyList()) - val accounts = _accounts.asStateFlow() - - private val _categories = MutableStateFlow(emptyList()) - val categories = _categories.asStateFlow() - - fun search(query: String) { - val normalizedQuery = query.lowercase().trim() - - viewModelScope.launch { - TestIdlingResource.increment() - - _baseCurrencyCode.value = baseCurrencyAct(Unit) - - _categories.value = categoriesAct(Unit) - - _accounts.value = accountsAct(Unit) - - _transactions.value = ioThread { - val trns = allTrnsAct(Unit) - .filter { - it.title.matchesQuery(normalizedQuery) || - it.description.matchesQuery(normalizedQuery) - } - trnsWithDateDivsAct( - TrnsWithDateDivsAct.Input( - baseCurrency = baseCurrencyCode.value, - transactions = trns - ) - ) - } - - TestIdlingResource.decrement() - } - } - - private fun String?.matchesQuery(query: String): Boolean { - return this?.lowercase()?.trim()?.contains(query) == true - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 19cf0e2742..61541fecc3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,45 +12,32 @@ include(":common:main") include(":common:android-test") include(":common:test") include(":design-system") -//include(":reports") include(":accounts") -//include(":categories") +include(":categories") include(":home:tab") include(":home:more-menu") include(":home:customer-journey") -//include(":planned-payments") -//include(":transaction-details") -//include(":pie-charts") -//include(":budgets") -//include(":loans") -//include(":settings") +include(":transaction") include(":onboarding") -//include(":item-transactions") -//include(":search-transactions") -//include(":donate") -include(":main") -include(":app-base") -include(":ui-components-old") -include(":temp-domain") -include(":temp-persistence") +include(":main:impl") +include(":main:base") include(":widgets") include(":app-locked") -//include(":balance-prediction") -//include(":import-csv-backup") -include(":temp-network") include(":billing") -include(":web-view") include(":android-notifications") include(":core:data-model") include(":core:domain") include(":core:exchange-provider") include(":core:ui") include(":core:persistence") -include(":sync:public") -include(":sync:base") -//include(":sync:ivy-server") include(":network") include(":resources") include(":navigation") include(":debug") -include(":formula") +include(":formula:domain") +include(":formula:persistence") +include(":formula:ui") +include(":parser") +include(":math") +include(":drive:google-drive") +include(":settings") \ No newline at end of file diff --git a/settings/build.gradle.kts b/settings/build.gradle.kts index b317706c74..5aa724f795 100644 --- a/settings/build.gradle.kts +++ b/settings/build.gradle.kts @@ -1,23 +1,21 @@ import com.ivy.buildsrc.Hilt +import com.ivy.buildsrc.Testing apply() plugins { `android-library` + `kotlin-android` } dependencies { Hilt() - implementation(project(":common")) + implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) + implementation(project(":core:domain")) implementation(project(":core:ui")) implementation(project(":core:data-model")) + implementation(project(":core:persistence")) implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":temp-network")) - implementation(project(":core:exchange-provider")) - implementation(project(":widgets")) + Testing() } \ No newline at end of file diff --git a/settings/src/main/java/com/ivy/settings/SettingsEvent.kt b/settings/src/main/java/com/ivy/settings/SettingsEvent.kt new file mode 100644 index 0000000000..af9f0b9ffa --- /dev/null +++ b/settings/src/main/java/com/ivy/settings/SettingsEvent.kt @@ -0,0 +1,10 @@ +package com.ivy.settings + +sealed interface SettingsEvent { + object Back : SettingsEvent + data class BaseCurrencyChange(val newCurrency: String) : SettingsEvent + data class StartDayOfMonth(val startDayOfMonth: Int) : SettingsEvent + data class HideBalance(val hideBalance: Boolean) : SettingsEvent + data class AppLocked(val appLocked: Boolean) : SettingsEvent +} + diff --git a/settings/src/main/java/com/ivy/settings/SettingsScreen.kt b/settings/src/main/java/com/ivy/settings/SettingsScreen.kt index ccac5535d1..36c61a5668 100644 --- a/settings/src/main/java/com/ivy/settings/SettingsScreen.kt +++ b/settings/src/main/java/com/ivy/settings/SettingsScreen.kt @@ -1,1354 +1,173 @@ package com.ivy.settings -import androidx.annotation.DrawableRes -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import coil.compose.AsyncImage -import com.ivy.base.Constants -import com.ivy.base.R -import com.ivy.base.names -import com.ivy.data.IvyCurrency -import com.ivy.data.user.AuthProviderType -import com.ivy.data.user.User -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.SunsetNight -import com.ivy.design.l0_system.style -import com.ivy.design.l1_buildingBlocks.IconScale -import com.ivy.design.l1_buildingBlocks.IvyIconScaled +import com.ivy.core.ui.currency.CurrencyPickerModal +import com.ivy.design.l1_buildingBlocks.H1 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.SwitchRow +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.BackButton +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.IvySwitch -import com.ivy.wallet.ui.theme.components.IvyToolbar -import com.ivy.wallet.ui.theme.modal.* -import com.ivy.wallet.utils.OpResult -import com.ivy.wallet.utils.clickableNoIndication -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.thenIf -import java.util.* - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.SettingsScreen() { +/* +- Dark Mode +- Hidden transactions (leads to new screen which you'll create later) +- Create Transaction steps (leads to new screen) +- Export backup +- Export CSV +- Import backup +- Exchange Rates (new screen) +- T&C + Privacy Policy +- Rate us +- Share Ivy Wallet +- Donate +- Ivy Telegram +- Delete all data +- GitHub repo + */ + +// H1, B1, Caption {H1Second, B1Second, ...} +// Focused, Medium +// IvyButton +// UI.color, UI.typo(fonts) +// SwitchRow (on/off) + +@Composable +fun BoxScope.SettingsScreen() { val viewModel: SettingsViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() - val user by viewModel.user.observeAsState() - val opSync by viewModel.opSync.observeAsState() - val currencyCode by viewModel.currencyCode.observeAsState("") - val lockApp by viewModel.lockApp.observeAsState(false) - val showNotifications by viewModel.showNotifications.collectAsState() - val hideCurrentBalance by viewModel.hideCurrentBalance.collectAsState() - val treatTransfersAsIncomeExpense by viewModel.treatTransfersAsIncomeExpense.collectAsState() - val startDateOfMonth by viewModel.startDateOfMonth.observeAsState(1) - val progressState by viewModel.progressState.collectAsState() - - val nameLocalAccount by viewModel.nameLocalAccount.observeAsState() - val opFetchTrns by viewModel.opFetchTrns.collectAsState() - - val rootScreen = com.ivy.core.ui.temp.rootScreen() - val context = LocalContext.current - UI( -// screen = screen, - user = user, - currencyCode = currencyCode, - opSync = opSync, - lockApp = lockApp, - showNotifications = showNotifications, - hideCurrentBalance = hideCurrentBalance, - progressState = progressState, - treatTransfersAsIncomeExpense = treatTransfersAsIncomeExpense, - - nameLocalAccount = nameLocalAccount, - startDateOfMonth = startDateOfMonth, - opFetchTrns = opFetchTrns, - - - onSetCurrency = viewModel::setCurrency, - onSetName = viewModel::setName, - - onSync = viewModel::sync, - onLogout = viewModel::logout, - onLogin = viewModel::login, - onBackupData = { - viewModel.exportToZip(context) - }, - onExportToCSV = { - viewModel.exportToCSV(context) - }, - onSetLockApp = viewModel::setLockApp, - onSetShowNotifications = viewModel::setShowNotifications, - onSetHideCurrentBalance = viewModel::setHideCurrentBalance, - onSetStartDateOfMonth = viewModel::setStartDateOfMonth, - onSetTreatTransfersAsIncExp = viewModel::setTransfersAsIncomeExpense, - onRequestFeature = { title, body -> - viewModel.requestFeature( - rootScreen = rootScreen, - title = title, - body = body - ) - }, - onDeleteAllUserData = viewModel::deleteAllUserData, - onDeleteCloudUserData = viewModel::deleteCloudUserData, - onFetchMissingTransactions = viewModel::fetchMissingTransactions - ) + UI(state = state, onEvent = viewModel::onEvent) } -@ExperimentalFoundationApi @Composable -private fun BoxWithConstraintsScope.UI( -// screen: SettingsScreen, - user: User?, - currencyCode: String, - opSync: OpResult?, - - lockApp: Boolean, - showNotifications: Boolean = true, - hideCurrentBalance: Boolean = false, - progressState: Boolean = false, - treatTransfersAsIncomeExpense: Boolean = false, - - nameLocalAccount: String?, - startDateOfMonth: Int = 1, - - opFetchTrns: OpResult? = null, - - onSetCurrency: (String) -> Unit, - onSetName: (String) -> Unit = {}, - - - onSync: () -> Unit, - onLogout: () -> Unit, - onLogin: () -> Unit, - onBackupData: () -> Unit = {}, - onExportToCSV: () -> Unit = {}, - onSetLockApp: (Boolean) -> Unit = {}, - onSetShowNotifications: (Boolean) -> Unit = {}, - onSetTreatTransfersAsIncExp: (Boolean) -> Unit = {}, - onSetHideCurrentBalance: (Boolean) -> Unit = {}, - onSetStartDateOfMonth: (Int) -> Unit = {}, - onRequestFeature: (String, String) -> Unit = { _, _ -> }, - onDeleteAllUserData: () -> Unit = {}, - onDeleteCloudUserData: () -> Unit = {}, - onFetchMissingTransactions: () -> Unit = {}, - - ) { - var currencyModalVisible by remember { mutableStateOf(false) } - var nameModalVisible by remember { mutableStateOf(false) } - var chooseStartDateOfMonthVisible by remember { mutableStateOf(false) } - var requestFeatureModalVisible by remember { mutableStateOf(false) } - var deleteCloudDataModalVisible by remember { mutableStateOf(false) } - var deleteAllDataModalVisible by remember { mutableStateOf(false) } - var deleteAllDataModalFinalVisible by remember { mutableStateOf(false) } +private fun BoxScope.UI( + state: SettingsState, + onEvent: (SettingsEvent) -> Unit +) { + val currencyModal = rememberIvyModal() + val startDayOfMonthModal = rememberIvyModal() LazyColumn( modifier = Modifier .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - .testTag("settings_lazy_column") + .systemBarsPadding() ) { - stickyHeader { - - IvyToolbar( - onBack = { nav.onBackPressed() }, - ) { - Spacer(Modifier.weight(1f)) - - Text( - modifier = Modifier.clickableNoIndication { -// nav.navigateTo(Test) - }, - text = "okay",//"${screen.versionName} (${screen.versionCode})", - style = UI.typoSecond.c.style( - color = UI.colors.neutral, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.width(32.dp)) - } - //onboarding toolbar include paddingBottom 16.dp - } - - item { - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.settings), - style = UI.typo.h2.style( - fontWeight = FontWeight.Black - ) - ) - - Spacer(Modifier.height(24.dp)) - - CurrencyButton(currency = currencyCode) { - currencyModalVisible = true - } - - Spacer(Modifier.height(12.dp)) - - AccountCard( - user = user, - opSync = opSync, - nameLocalAccount = nameLocalAccount, - - onSync = onSync, - onLogout = onLogout, - onLogin = onLogin, - ) { - nameModalVisible = true - } - -// Spacer(Modifier.height(20.dp)) -// Premium() - } - - item { - SettingsSectionDivider(text = "Sync") - - Spacer(Modifier.height(16.dp)) - - FetchMissingTransactionsButton( - opFetchTrns = opFetchTrns, - onFetchMissingTransactions = onFetchMissingTransactions - ) - } - - item { - SettingsSectionDivider(text = stringResource(R.string.import_export)) - - Spacer(Modifier.height(16.dp)) - - - ExportCSV { - onExportToCSV() - } - - Spacer(Modifier.height(12.dp)) - - SettingsDefaultButton( - icon = R.drawable.ic_vue_security_shield, - text = stringResource(R.string.backup_data), - iconPadding = 6.dp - ) { - onBackupData() + item(key = "content") { + BackButton(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + onEvent(SettingsEvent.Back) } - Spacer(Modifier.height(12.dp)) - - SettingsPrimaryButton( - icon = R.drawable.ic_export_csv, - text = stringResource(R.string.import_data), - backgroundGradient = GradientGreen - ) { -// nav.navigateTo( -// Import( -// launchedFromOnboarding = false -// ) -// ) - } - } - - item { - SettingsSectionDivider(text = stringResource(R.string.app_settings)) - - Spacer(Modifier.height(16.dp)) - - AppSwitch( - lockApp = lockApp, - onSetLockApp = onSetLockApp, - text = stringResource(R.string.lock_app), - icon = R.drawable.ic_custom_fingerprint_m - ) - - Spacer(Modifier.height(12.dp)) - - AppSwitch( - lockApp = showNotifications, - onSetLockApp = onSetShowNotifications, - text = stringResource(R.string.show_notifications), - icon = R.drawable.ic_notification_m - ) - - Spacer(Modifier.height(12.dp)) - - AppSwitch( - lockApp = hideCurrentBalance, - onSetLockApp = onSetHideCurrentBalance, - text = stringResource(R.string.hide_balance), - description = stringResource(R.string.hide_balance_description), - icon = R.drawable.ic_hide_m + H1( + text = "Settings", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) - - Spacer(Modifier.height(12.dp)) - - AppSwitch( - lockApp = treatTransfersAsIncomeExpense, - onSetLockApp = onSetTreatTransfersAsIncExp, - text = stringResource(R.string.transfers_as_income_expense), - description = stringResource(R.string.transfers_as_income_expense_description), - icon = R.drawable.ic_custom_transfer_m - ) - - Spacer(Modifier.height(12.dp)) - - StartDateOfMonth( - startDateOfMonth = startDateOfMonth - ) { - chooseStartDateOfMonthVisible = true - } - } - -// item { -// SettingsSectionDivider(text = stringResource(R.string.experimental)) -// -// Spacer(Modifier.height(16.dp)) -// -// -// SettingsDefaultButton( -// icon = R.drawable.ic_custom_atom_m, -// text = stringResource(R.string.experimental_settings) -// ) { -// nav.navigateTo(ExperimentalScreen) -// } -// } - - item { - SettingsSectionDivider(text = stringResource(R.string.other)) - - Spacer(Modifier.height(16.dp)) - - val rootScreen = com.ivy.core.ui.temp.rootScreen() - SettingsPrimaryButton( - icon = R.drawable.ic_custom_star_m, - text = stringResource(R.string.rate_us_on_google_play), - backgroundGradient = GradientIvy - ) { - rootScreen.reviewIvyWallet(dismissReviewCard = false) - } - - Spacer(Modifier.height(12.dp)) - - SettingsPrimaryButton( - icon = R.drawable.ic_custom_family_m, - text = stringResource(R.string.share_ivy_wallet), - backgroundGradient = Gradient.solid(Red3) - ) { - rootScreen.shareIvyWallet() - } - - Spacer(Modifier.height(12.dp)) - - - SettingsPrimaryButton( - icon = R.drawable.ic_donate_crown, - text = "Donate", - iconPadding = 8.dp, - backgroundGradient = Gradient.from(SunsetNight) + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, visibility = Visibility.Medium, + feeling = Feeling.Positive, text = state.baseCurrency, icon = null ) { -// nav.navigateTo(DonateScreen) - } - } - - item { - SettingsSectionDivider(text = stringResource(R.string.product)) - - Spacer(Modifier.height(12.dp)) - - IvyTelegram() - - Spacer(Modifier.height(16.dp)) - - HelpCenter() - - Spacer(Modifier.height(12.dp)) - - Roadmap() - - Spacer(Modifier.height(12.dp)) - - RequestFeature { - requestFeatureModalVisible = true + currencyModal.show() } - - Spacer(Modifier.height(12.dp)) - - ContactSupport() - - Spacer(Modifier.height(12.dp)) - - ProjectContributors() - - Spacer(Modifier.height(12.dp)) - - TCAndPrivacyPolicy() - } - - item { - SettingsSectionDivider( - text = stringResource(R.string.danger_zone), - color = Red - ) - - Spacer(Modifier.height(16.dp)) - - SettingsPrimaryButton( - icon = R.drawable.ic_delete, - text = stringResource(R.string.delete_all_user_data), - backgroundGradient = Gradient.solid(Red) + SpacerVer(height = 12.dp) + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, visibility = Visibility.Focused, + feeling = Feeling.Positive, text = "Start day of month ${state.startDayOfMonth}", + icon = null ) { - deleteAllDataModalVisible = true + startDayOfMonthModal.show() } - - if (user != null) { - Spacer(Modifier.height(16.dp)) - - SettingsPrimaryButton( - icon = R.drawable.ic_categories, - text = stringResource(R.string.switch_to_offline_mode), - backgroundGradient = Gradient.solid(Red) - ) { - deleteCloudDataModalVisible = true - } - } - } - - item { - Spacer(modifier = Modifier.height(120.dp)) //last item spacer + SpacerVer(height = 12.dp) + SwitchRow(enabled = state.hideBalance, text = "Hide balance", onValueChange = { + onEvent(SettingsEvent.HideBalance(hideBalance = it)) + }) + SpacerVer(height = 12.dp) + SwitchRow(enabled = state.appLocked, text = "Lock app", onValueChange = { + onEvent(SettingsEvent.AppLocked(appLocked = it)) + }) } } - CurrencyModal( - title = stringResource(R.string.set_currency), - initialCurrency = IvyCurrency.fromCode(currencyCode), - visible = currencyModalVisible, - dismiss = { currencyModalVisible = false } - ) { - onSetCurrency(it) - } - - NameModal( - visible = nameModalVisible, - name = nameLocalAccount ?: "", - dismiss = { nameModalVisible = false } - ) { - onSetName(it) - } - - ChooseStartDateOfMonthModal( - visible = chooseStartDateOfMonthVisible, - selectedStartDateOfMonth = startDateOfMonth, - dismiss = { chooseStartDateOfMonthVisible = false } - ) { - onSetStartDateOfMonth(it) - } - - RequestFeatureModal( - visible = requestFeatureModalVisible, - dismiss = { - requestFeatureModalVisible = false - }, - onSubmit = onRequestFeature - ) - - DeleteModal( - title = stringResource(R.string.delete_all_user_data_question), - description = stringResource( - R.string.delete_all_user_data_warning, user?.email ?: stringResource( - R.string.your_account - ) - ), - visible = deleteAllDataModalVisible, - dismiss = { deleteAllDataModalVisible = false }, - onDelete = { - deleteAllDataModalVisible = false - deleteAllDataModalFinalVisible = true - } - ) - - DeleteModal( - title = stringResource( - R.string.confirm_all_userd_data_deletion, user?.email ?: stringResource( - R.string.all_of_your_data - ) - ), - description = stringResource(R.string.final_deletion_warning), - visible = deleteAllDataModalFinalVisible, - dismiss = { deleteAllDataModalFinalVisible = false }, - onDelete = { - onDeleteAllUserData() - } - ) - - DeleteModal( - title = stringResource(R.string.delete_all_cloud_data_question), - description = stringResource( - R.string.delete_all_user_cloud_data_warning, user?.email ?: stringResource( - R.string.your_account - ) - ), - visible = deleteCloudDataModalVisible, - dismiss = { deleteCloudDataModalVisible = false }, - onDelete = { - onDeleteCloudUserData() - deleteCloudDataModalVisible = false + CurrencyPickerModal( + modal = currencyModal, + initialCurrency = state.baseCurrency, + onCurrencyPick = { newCurrency -> + onEvent(SettingsEvent.BaseCurrencyChange(newCurrency = newCurrency)) } ) - ProgressModal( - title = stringResource(R.string.exporting_data), - description = stringResource(R.string.exporting_data_description), - visible = progressState - ) -} - -@Composable -private fun StartDateOfMonth( - startDateOfMonth: Int, - onClick: () -> Unit -) { - SettingsButtonRow( - onClick = onClick - ) { - Spacer(Modifier.width(12.dp)) - - IvyIconScaled( - icon = R.drawable.ic_custom_calendar_m, - tint = UI.colorsInverted.pure, - iconScale = IconScale.M, - padding = 0.dp - ) - - Spacer(Modifier.width(8.dp)) - - Text( - modifier = Modifier.padding(vertical = 20.dp), - text = stringResource(R.string.start_date_of_month), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.weight(1f)) - - Text( - text = startDateOfMonth.toString(), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.width(32.dp)) - } -} - - -@Composable -private fun IvyTelegram() { - val rootActivity = com.ivy.core.ui.temp.rootScreen() - SettingsPrimaryButton( - icon = R.drawable.ic_telegram_24dp, - text = stringResource(R.string.ivy_telegram), - backgroundGradient = Gradient.solid(Blue), - iconPadding = 8.dp - ) { - rootActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE) - } -} - -@Composable -private fun HelpCenter() { - - SettingsDefaultButton( - icon = R.drawable.ic_custom_education_m, - text = stringResource(R.string.help_center), - ) { -// nav.navigateTo( -// IvyWebView(url = Constants.URL_HELP_CENTER) -// ) - } -} - -@Composable -private fun Roadmap() { - - SettingsDefaultButton( - icon = R.drawable.ic_custom_rocket_m, - text = stringResource(R.string.roadmap), - ) { -// nav.navigateTo( -// IvyWebView(url = Constants.URL_ROADMAP) -// ) - } -} - -@Composable -private fun RequestFeature( - onClick: () -> Unit -) { - SettingsDefaultButton( - icon = R.drawable.ic_custom_programming_m, - text = stringResource(R.string.request_a_feature), - ) { - onClick() - } -} - -@Composable -private fun ContactSupport() { - val rootActivity = com.ivy.core.ui.temp.rootScreen() - SettingsDefaultButton( - icon = R.drawable.ic_support, - text = stringResource(R.string.contact_support), - ) { - rootActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE) - } -} - -@Composable -private fun ProjectContributors() { - - SettingsDefaultButton( - icon = R.drawable.ic_vue_people_people, - text = stringResource(R.string.project_contributors), - iconPadding = 6.dp - ) { -// nav.navigateTo( -// IvyWebView(url = URL_IVY_CONTRIBUTORS) -// ) - } -} - -@Composable -private fun AppSwitch( - lockApp: Boolean, - onSetLockApp: (Boolean) -> Unit, - text: String, - description: String = "", - icon: Int, -) { - SettingsButtonRow( - onClick = { - onSetLockApp(!lockApp) - } - ) { - Spacer(Modifier.width(12.dp)) - - IvyIconScaled( - icon = icon, - tint = UI.colorsInverted.pure, - iconScale = IconScale.M, - padding = 0.dp - ) - - Spacer(Modifier.width(8.dp)) - - Column( - Modifier - .weight(1f) - .padding(top = 20.dp, bottom = 20.dp, end = 8.dp) - ) { - Text( - text = text, - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) - if (description.isNotEmpty()) { - Text( - modifier = Modifier.padding(end = 8.dp), - text = description, - style = UI.typoSecond.b2.style( - color = Gray, - fontWeight = FontWeight.Normal - ).copy(fontSize = 14.sp) - ) - } - } - - //Spacer(Modifier.weight(1f)) - - IvySwitch(enabled = lockApp) { - onSetLockApp(it) - } - - Spacer(Modifier.width(16.dp)) - } -} - -@Composable -private fun AccountCard( - user: User?, - opSync: OpResult?, - nameLocalAccount: String?, - - onSync: () -> Unit, - onLogout: () -> Unit, - onLogin: () -> Unit, - onCardClick: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .fillMaxWidth() - .clip(UI.shapes.rounded) - .background(UI.colors.medium, UI.shapes.rounded) - .clickable { - onCardClick() - } - ) { - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("settings_profile_card"), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - Text( - text = stringResource(R.string.account_uppercase), - style = UI.typo.c.style( - fontWeight = FontWeight.Black, - color = UI.colors.neutral - ) - ) - - Spacer(Modifier.weight(1f)) - - if (user != null) { - AccountCardButton( - icon = R.drawable.ic_logout, - text = stringResource(R.string.logout) - ) { - onLogout() - } - } else { - AccountCardButton( - icon = R.drawable.ic_login, - text = stringResource(R.string.login) - ) { - onLogin() - } - } - - Spacer(Modifier.width(16.dp)) - } - - if (user != null) { - AccountCardUser( - localName = nameLocalAccount, - user = user, - opSync = opSync, - onSync = onSync - ) - } else { - AccountCardLocalAccount( - name = nameLocalAccount, - ) - } - - } + StartDayOfMonthModal( + modal = startDayOfMonthModal, + onStartDayOfMonthChange = { startDayOfMonth -> + onEvent(SettingsEvent.StartDayOfMonth(startDayOfMonth = startDayOfMonth)) + }) } @Composable -private fun AccountCardUser( - localName: String?, - user: User, - opSync: OpResult?, - - onSync: () -> Unit, +private fun BoxScope.StartDayOfMonthModal( + modal: IvyModal, + onStartDayOfMonthChange: (Int) -> Unit, ) { - Spacer(Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - if (user.profilePicture != null) { - AsyncImage( - modifier = Modifier - .clip(CircleShape) - .size(32.dp), - model = user.profilePicture, - contentScale = ContentScale.FillBounds, - contentDescription = "profile picture" - ) - - Spacer(Modifier.width(12.dp)) + Modal( + modal = modal, + actions = { } - - Text( - text = localName ?: user.names(), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.height(12.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(20.dp)) - - IvyIconScaled( - icon = R.drawable.ic_email, - iconScale = IconScale.S, - padding = 0.dp - ) - - Spacer(Modifier.width(12.dp)) - - Text( - text = user.email, - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.height(12.dp)) - - when (opSync) { - is OpResult.Loading -> { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - IvyIconScaled( - icon = R.drawable.ic_data_synced, - tint = Orange, - iconScale = IconScale.S, - padding = 0.dp - ) - - Spacer(Modifier.width(12.dp)) - - Text( - text = stringResource(R.string.syncing), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = Orange - ) - ) - - Spacer(Modifier.width(24.dp)) - } - } - is OpResult.Success -> { - if (opSync.data) { - //synced - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - IvyIconScaled( - icon = R.drawable.ic_data_synced, - tint = Green, - iconScale = IconScale.S, - padding = 0.dp - ) - - Spacer(Modifier.width(12.dp)) - - Text( - text = stringResource(R.string.data_synced_to_cloud), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = Green - ) - ) - - Spacer(Modifier.width(24.dp)) - } - } else { - //not synced + Title(text = "Set start day of month") + SpacerVer(height = 24.dp) + LazyColumn { + items(items = (1..31).toList()) { number -> + SpacerVer(height = 12.dp) IvyButton( - modifier = Modifier.padding(horizontal = 24.dp), - iconStart = R.drawable.ic_sync, - text = stringResource(R.string.tap_to_sync), - backgroundGradient = GradientRed + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = number.toString(), + icon = null ) { - onSync() + onStartDayOfMonthChange(number) + modal.hide() } } } - is OpResult.Failure -> { - IvyButton( - modifier = Modifier.padding(horizontal = 24.dp), - iconStart = R.drawable.ic_sync, - text = stringResource(R.string.sync_failed), - backgroundGradient = GradientRed - ) { - onSync() - } - } - null -> {} - } - - Spacer(Modifier.height(24.dp)) -} - -@Composable -private fun AccountCardLocalAccount( - name: String?, -) { - Spacer(Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - IvyIconScaled( - icon = R.drawable.ic_local_account, - iconScale = IconScale.M - ) - - Spacer(Modifier.width(12.dp)) - - Text( - modifier = Modifier.testTag("local_account_name"), - text = if (name != null && name.isNotBlank()) name else stringResource(R.string.anonymous), - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold - ) - ) + SpacerVer(height = 24.dp) } - - Spacer(Modifier.height(24.dp)) } -@Composable -private fun ExportCSV( - onExportToCSV: () -> Unit -) { - SettingsDefaultButton( - icon = R.drawable.ic_vue_pc_printer, - text = stringResource(R.string.export_to_csv), - iconPadding = 6.dp - ) { - onExportToCSV() - } -} - -@Composable -private fun TCAndPrivacyPolicy() { - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - val uriHandler = LocalUriHandler.current - - Text( - modifier = Modifier - .weight(1f) - .clip(UI.shapes.fullyRounded) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - .clickable { - uriHandler.openUri(Constants.URL_TC) - } - .padding(vertical = 14.dp), - text = stringResource(R.string.terms_conditions), - style = UI.typo.c.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure, - textAlign = TextAlign.Center - ) - ) - - Spacer(Modifier.width(12.dp)) - - Text( - modifier = Modifier - .weight(1f) - .clip(UI.shapes.fullyRounded) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - .clickable { - uriHandler.openUri(Constants.URL_PRIVACY_POLICY) - } - .padding(vertical = 14.dp), - text = stringResource(R.string.privacy_policy), - style = UI.typo.c.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure, - textAlign = TextAlign.Center - ) - ) - - Spacer(Modifier.width(16.dp)) - } -} -@Composable -private fun SettingsPrimaryButton( - @DrawableRes icon: Int, - text: String, - hasShadow: Boolean = false, - backgroundGradient: Gradient = Gradient.solid(UI.colors.medium), - textColor: Color = White, - iconPadding: Dp = 0.dp, - onClick: () -> Unit -) { - SettingsButtonRow( - hasShadow = hasShadow, - backgroundGradient = backgroundGradient, - onClick = onClick - ) { - Spacer(Modifier.width(12.dp)) - - IvyIconScaled( - icon = icon, - tint = textColor, - iconScale = IconScale.M, - padding = iconPadding - ) - - Spacer(Modifier.width(8.dp)) - - Text( - modifier = Modifier.padding(vertical = 20.dp), - text = text, - style = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.Bold - ) - ) - } -} - -@Composable -private fun SettingsButtonRow( - hasShadow: Boolean = false, - backgroundGradient: Gradient = Gradient.solid(UI.colors.medium), - onClick: (() -> Unit)?, - Content: @Composable RowScope.() -> Unit -) { - Row( - modifier = Modifier - .padding(horizontal = 16.dp) - .thenIf(hasShadow) { - drawColoredShadow(color = backgroundGradient.startColor) - } - .fillMaxWidth() - .clip(UI.shapes.squared) - .background(backgroundGradient.asHorizontalBrush(), UI.shapes.squared) - .thenIf(onClick != null) { - clickable { - onClick?.invoke() - } - }, - verticalAlignment = Alignment.CenterVertically - ) { - Content() - } -} - -@Composable -private fun AccountCardButton( - @DrawableRes icon: Int, - text: String, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .background(UI.colors.pure, UI.shapes.fullyRounded) - .clickable { - onClick() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - IvyIconScaled( - icon = icon, - iconScale = IconScale.M - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier - .padding(vertical = 10.dp), - text = text, - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.width(24.dp)) - } -} - -@Composable -private fun CurrencyButton( - currency: String, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .border(2.dp, UI.colors.medium, UI.shapes.squared) - .clickable { - onClick() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - IvyIconScaled( - icon = R.drawable.ic_currency, - iconScale = IconScale.M, - padding = 0.dp - ) - - Spacer(Modifier.width(8.dp)) - - Text( - modifier = Modifier.padding(vertical = 20.dp), - text = stringResource(R.string.set_currency), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.weight(1f)) - - Text( - text = currency, - style = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(4.dp)) - - IvyIconScaled( - icon = R.drawable.ic_arrow_right, - iconScale = IconScale.M - ) - - Spacer(Modifier.width(24.dp)) - } -} - -@Composable -private fun SettingsSectionDivider( - text: String, - color: Color = Gray -) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = text, - style = UI.typo.b2.style( - color = color, - fontWeight = FontWeight.Bold - ) - ) -} - -@Composable -fun FetchMissingTransactionsButton( - opFetchTrns: OpResult?, - onFetchMissingTransactions: () -> Unit -) { - val background = Gradient.solid( - when (opFetchTrns) { - is OpResult.Failure -> Red - OpResult.Loading -> Orange - is OpResult.Success -> Green - null -> UI.colors.medium - } - ) - SettingsPrimaryButton( - icon = R.drawable.ic_sync, - text = when (opFetchTrns) { - is OpResult.Failure -> "Error: ${opFetchTrns.error()}" - OpResult.Loading -> "Full sync... wait!" - is OpResult.Success -> "Success. Check transactions." - else -> "Fetch missing transactions" - }, - backgroundGradient = background, - textColor = findContrastTextColor(background.startColor), - iconPadding = 0.dp - ) { - onFetchMissingTransactions() - } -} - -@Composable -private fun SettingsDefaultButton( - @DrawableRes icon: Int, - text: String, - iconPadding: Dp = 0.dp, - onClick: () -> Unit -) { - SettingsPrimaryButton( - icon = icon, - text = text, - backgroundGradient = Gradient.solid(UI.colors.medium), - textColor = UI.colorsInverted.pure, - iconPadding = iconPadding - ) { - onClick() - } -} - -@ExperimentalFoundationApi @Preview @Composable -private fun Preview_synced() { +private fun Preview() { IvyPreview { UI( -// screen = SettingsScreen(versionName = "1.0.0", versionCode = "100"), - user = User( - email = "iliyan.germanov971@gmail.com", - authProviderType = AuthProviderType.GOOGLE, - firstName = "Iliyan", - lastName = "Germanov", - color = 11, - id = UUID.randomUUID(), - profilePicture = null + state = SettingsState( + baseCurrency = "BGN", + startDayOfMonth = 1, + hideBalance = false, + appLocked = false ), - nameLocalAccount = null, - opSync = OpResult.success(true), - lockApp = false, - currencyCode = "BGN", - onSetCurrency = {}, - onLogout = {}, - onLogin = {}, - onSync = {} - ) - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview_notSynced() { - IvyPreview { - UI( -// screen = SettingsScreen(versionName = "1.0.0", versionCode = "100"), - user = User( - email = "iliyan.germanov971@gmail.com", - authProviderType = AuthProviderType.GOOGLE, - firstName = "Iliyan", - lastName = "Germanov", - color = 11, - id = UUID.randomUUID(), - profilePicture = null - ), - lockApp = false, - nameLocalAccount = null, - opSync = OpResult.success(false), - currencyCode = "BGN", - onSetCurrency = {}, - onLogout = {}, - onLogin = {}, - onSync = {} - ) - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview_loading() { - IvyPreview { - UI( -// screen = SettingsScreen(versionName = "1.0.0", versionCode = "100"), - user = User( - email = "iliyan.germanov971@gmail.com", - authProviderType = AuthProviderType.GOOGLE, - firstName = "Iliyan", - lastName = null, - color = 11, - id = UUID.randomUUID(), - profilePicture = null - ), - lockApp = false, - nameLocalAccount = null, - opSync = OpResult.loading(), - currencyCode = "BGN", - onSetCurrency = {}, - onLogout = {}, - onLogin = {}, - onSync = {} - ) - } -} - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview_localAccount() { - IvyPreview { - UI( -// screen = SettingsScreen(versionName = "1.0.0", versionCode = "100"), - user = null, - nameLocalAccount = "Iliyan", - opSync = null, - currencyCode = "BGN", - lockApp = false, - onSetCurrency = {}, - onLogout = {}, - onLogin = {}, - onSync = {} + onEvent = {} ) } } \ No newline at end of file diff --git a/settings/src/main/java/com/ivy/settings/SettingsState.kt b/settings/src/main/java/com/ivy/settings/SettingsState.kt new file mode 100644 index 0000000000..dfc2a2df6f --- /dev/null +++ b/settings/src/main/java/com/ivy/settings/SettingsState.kt @@ -0,0 +1,8 @@ +package com.ivy.settings + +data class SettingsState( + val baseCurrency: String, + val startDayOfMonth: Int, + val hideBalance: Boolean, + val appLocked: Boolean +) \ No newline at end of file diff --git a/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt b/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt index 8dbe35fc85..1b633cba0b 100644 --- a/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt +++ b/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt @@ -1,417 +1,67 @@ package com.ivy.settings -import android.content.Context -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.data.user.User -import com.ivy.frp.monad.Res -import com.ivy.frp.test.TestIdlingResource - -import com.ivy.wallet.domain.action.global.StartDayOfMonthAct -import com.ivy.wallet.domain.action.global.UpdateStartDayOfMonthAct -import com.ivy.wallet.domain.action.transaction.FetchAllTrnsFromServerAct -import com.ivy.wallet.domain.deprecated.logic.csv.ExportCSVLogic -import com.ivy.wallet.domain.deprecated.logic.currency.ExchangeRatesLogic -import com.ivy.wallet.domain.deprecated.logic.zip.ExportZipLogic -import com.ivy.wallet.domain.deprecated.sync.IvySync -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.request.auth.GoogleSignInRequest -import com.ivy.wallet.io.network.request.github.OpenIssueRequest -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.SettingsDao -import com.ivy.wallet.io.persistence.dao.UserDao -import com.ivy.wallet.utils.* -import com.ivy.widgets.WalletBalanceReceiver +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.settings.applocked.AppLockedFlow +import com.ivy.core.domain.action.settings.applocked.WriteAppLockedAct +import com.ivy.core.domain.action.settings.balance.HideBalanceFlow +import com.ivy.core.domain.action.settings.balance.WriteHideBalanceAct +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyFlow +import com.ivy.core.domain.action.settings.basecurrency.WriteBaseCurrencyAct +import com.ivy.core.domain.action.settings.startdayofmonth.StartDayOfMonthFlow +import com.ivy.core.domain.action.settings.startdayofmonth.WriteStartDayOfMonthAct +import com.ivy.navigation.Navigator import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import timber.log.Timber +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( - private val settingsDao: SettingsDao, - private val ivySession: IvySession, - private val userDao: UserDao, - private val ivyContext: IvyWalletCtx, - private val ivySync: IvySync, - private val exportCSVLogic: ExportCSVLogic, - private val restClient: RestClient, - private val exchangeRatesLogic: ExchangeRatesLogic, - private val logoutLogic: LogoutLogic, - private val sharedPrefs: SharedPrefs, - private val exportZipLogic: ExportZipLogic, - private val startDayOfMonthAct: StartDayOfMonthAct, - private val updateStartDayOfMonthAct: UpdateStartDayOfMonthAct, - private val fetchAllTrnsFromServerAct: FetchAllTrnsFromServerAct, - private val nav: Navigation -) : ViewModel() { - - private val _user = MutableLiveData() - val user = _user.asLiveData() - - private val _nameLocalAccount = MutableLiveData() - val nameLocalAccount = _nameLocalAccount.asLiveData() - - private val _opSync = MutableLiveData>() - val opSync = _opSync.asLiveData() - - private val _currencyCode = MutableLiveData() - val currencyCode = _currencyCode.asLiveData() - - private val _lockApp = MutableLiveData() - val lockApp = _lockApp.asLiveData() - - private val _hideCurrentBalance = MutableStateFlow(false) - val hideCurrentBalance = _hideCurrentBalance.asStateFlow() - - private val _showNotifications = MutableStateFlow(true) - val showNotifications = _showNotifications.asStateFlow() - - private val _treatTransfersAsIncomeExpense = MutableStateFlow(false) - val treatTransfersAsIncomeExpense = _treatTransfersAsIncomeExpense.asStateFlow() - - private val _progressState = MutableStateFlow(false) - val progressState = _progressState.asStateFlow() - - private val _startDateOfMonth = MutableLiveData() - val startDateOfMonth = _startDateOfMonth - - private val _opFetchtrns = MutableStateFlow?>(null) - val opFetchTrns = _opFetchtrns.asStateFlow() - - fun start() { - viewModelScope.launch { - TestIdlingResource.increment() - - val settings = ioThread { settingsDao.findFirstSuspend() } - - _nameLocalAccount.value = settings.name - - _startDateOfMonth.value = startDayOfMonthAct(Unit)!! - - _user.value = ioThread { - val userId = ivySession.getUserIdSafe() - if (userId != null) userDao.findById(userId)?.toDomain() else null - } - _currencyCode.value = settings.currency - - _lockApp.value = sharedPrefs.getBoolean(SharedPrefs.APP_LOCK_ENABLED, false) - _hideCurrentBalance.value = - sharedPrefs.getBoolean(SharedPrefs.HIDE_CURRENT_BALANCE, false) - - _showNotifications.value = sharedPrefs.getBoolean(SharedPrefs.SHOW_NOTIFICATIONS, true) - - _treatTransfersAsIncomeExpense.value = - sharedPrefs.getBoolean(SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, false) - - _opSync.value = OpResult.success(ioThread { ivySync.isSynced() }) - - TestIdlingResource.decrement() - } - } - - fun sync() { - viewModelScope.launch { - TestIdlingResource.increment() - - _opSync.value = OpResult.loading() - - ioThread { - ivySync.sync() - } - - _opSync.value = OpResult.success(ioThread { ivySync.isSynced() }) - - TestIdlingResource.decrement() - } - } - - - fun setName(newName: String) { - viewModelScope.launch { - TestIdlingResource.increment() - - ioThread { - settingsDao.save( - settingsDao.findFirstSuspend().copy( - name = newName - ) - ) - } - start() - - TestIdlingResource.decrement() - } - } - - fun setCurrency(newCurrency: String) { - viewModelScope.launch { - TestIdlingResource.increment() - - ioThread { - settingsDao.save( - settingsDao.findFirstSuspend().copy( - currency = newCurrency - ) - ) - - exchangeRatesLogic.sync(baseCurrency = newCurrency) - } - start() - - TestIdlingResource.decrement() - } - } - - fun exportToCSV(context: Context) { - ivyContext.createNewFile( - "Ivy Wallet (${ - timeNowUTC().formatNicelyWithTime(noWeekDay = true) - }).csv" - ) { fileUri -> - viewModelScope.launch { - TestIdlingResource.increment() - - exportCSVLogic.exportToFile( - context = context, - fileUri = fileUri - ) - - (context as com.ivy.core.ui.temp.RootScreen).shareCSVFile( - fileUri = fileUri - ) - - TestIdlingResource.decrement() + private val navigator: Navigator, + private val baseCurrencyFlow: BaseCurrencyFlow, + private val writeBaseCurrencyAct: WriteBaseCurrencyAct, + private val startDayOfMonthFlow: StartDayOfMonthFlow, + private val writeStartDayOfMonthAct: WriteStartDayOfMonthAct, + private val hideBalanceFlow: HideBalanceFlow, + private val writeHideBalanceAct: WriteHideBalanceAct, + private val appLockedFlow: AppLockedFlow, + private val writeAppLockedAct: WriteAppLockedAct +) : SimpleFlowViewModel() { + override val initialUi: SettingsState = SettingsState( + baseCurrency = "", + startDayOfMonth = 1, + hideBalance = false, + appLocked = false + ) + + override val uiFlow: Flow = combine( + baseCurrencyFlow(), + startDayOfMonthFlow(), + hideBalanceFlow(Unit), + appLockedFlow(Unit) + ) { baseCurrency, startDayOfMonth, hideBalance, appLocked -> + SettingsState( + baseCurrency = baseCurrency, + startDayOfMonth = startDayOfMonth, + hideBalance = hideBalance, + appLocked = appLocked + ) + } + + override suspend fun handleEvent(event: SettingsEvent) { + when (event) { + SettingsEvent.Back -> navigator.back() + is SettingsEvent.BaseCurrencyChange -> { + writeBaseCurrencyAct(event.newCurrency) } - } - } - - fun exportToZip(context: Context) { - ivyContext.createNewFile( - "Ivy Wallet (${ - timeNowUTC().formatNicelyWithTime(noWeekDay = true) - }).zip" - ) { fileUri -> - viewModelScope.launch(Dispatchers.IO) { - TestIdlingResource.increment() - - _progressState.value = true - exportZipLogic.exportToFile(context = context, zipFileUri = fileUri) - _progressState.value = false - - uiThread { - (context as com.ivy.core.ui.temp.RootScreen).shareZipFile( - fileUri = fileUri - ) - } - - TestIdlingResource.decrement() + is SettingsEvent.StartDayOfMonth -> { + writeStartDayOfMonthAct(event.startDayOfMonth) } - } - } - - - fun setStartDateOfMonth(startDate: Int) { - viewModelScope.launch { - TestIdlingResource.increment() - - when (val res = updateStartDayOfMonthAct(startDate)) { - is Res.Err -> {} - is Res.Ok -> { - _startDateOfMonth.value = res.data!! - } + is SettingsEvent.HideBalance -> { + writeHideBalanceAct(event.hideBalance) } - - TestIdlingResource.decrement() - } - } - - fun logout() { - viewModelScope.launch { - TestIdlingResource.increment() - - logoutLogic.logout() - - TestIdlingResource.decrement() - } - } - - fun cloudLogout() { - viewModelScope.launch { - TestIdlingResource.increment() - - logoutLogic.cloudLogout() - - TestIdlingResource.decrement() - } - } - - fun login() { - ivyContext.googleSignIn { idToken -> - if (idToken != null) { - viewModelScope.launch { - TestIdlingResource.increment() - - try { - val authResponse = restClient.authService.googleSignIn( - GoogleSignInRequest( - googleIdToken = idToken, - fcmToken = "n/a" - ) - ) - - ioThread { - ivySession.initiate(authResponse) - - settingsDao.save( - settingsDao.findFirstSuspend().copy( - name = authResponse.user.firstName - ) - ) - } - - start() - - sync() - } catch (e: Exception) { - e.printStackTrace() - Timber.e("Settings - Login with Google failed on Ivy server - ${e.message}") - } - - TestIdlingResource.decrement() - } - } else { - Timber.e("Settings - Login with Google failed while getting idToken") - } - } - } - - fun setLockApp(lockApp: Boolean) { - viewModelScope.launch { - TestIdlingResource.increment() - - sharedPrefs.putBoolean(SharedPrefs.APP_LOCK_ENABLED, lockApp) - _lockApp.value = lockApp - com.ivy.core.ui.temp.refreshWidget(WalletBalanceReceiver::class.java) - - TestIdlingResource.decrement() - } - } - - fun setShowNotifications(showNotifications: Boolean) { - viewModelScope.launch { - TestIdlingResource.increment() - - sharedPrefs.putBoolean(SharedPrefs.SHOW_NOTIFICATIONS, showNotifications) - _showNotifications.value = showNotifications - - TestIdlingResource.decrement() - } - } - - fun setHideCurrentBalance(hideCurrentBalance: Boolean) { - viewModelScope.launch { - TestIdlingResource.increment() - - sharedPrefs.putBoolean(SharedPrefs.HIDE_CURRENT_BALANCE, hideCurrentBalance) - _hideCurrentBalance.value = hideCurrentBalance - - TestIdlingResource.decrement() - } - } - - fun setTransfersAsIncomeExpense(treatTransfersAsIncomeExpense: Boolean) { - viewModelScope.launch { - TestIdlingResource.increment() - - sharedPrefs.putBoolean( - SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, - treatTransfersAsIncomeExpense - ) - _treatTransfersAsIncomeExpense.value = treatTransfersAsIncomeExpense - - TestIdlingResource.decrement() - } - } - - fun requestFeature( - rootScreen: com.ivy.core.ui.temp.RootScreen, - title: String, - body: String - ) { - viewModelScope.launch { - TestIdlingResource.increment() - - try { - val response = restClient.githubService.openIssue( - request = OpenIssueRequest( - title = title, - body = body, - ) - ) - - //Returned: https://api.github.com/repos/octocat/Hello-World/issues/1347 - //Should open: https://github.com/octocat/Hello-World/issues/1347 - val issueUrl = response.url - .replace("api.github.com", "github.com") - .replace("/repos", "") - - rootScreen.openUrlInBrowser(issueUrl) - } catch (e: Exception) { - e.printStackTrace() - } - - TestIdlingResource.decrement() - } - } - - fun deleteAllUserData() { - viewModelScope.launch { - try { - restClient.nukeService.deleteAllUserData() - } catch (e: Exception) { - e.printStackTrace() - } - logout() - } - } - - fun deleteCloudUserData() { - viewModelScope.launch { - try { - restClient.nukeService.deleteAllUserData() - } catch (e: Exception) { - e.printStackTrace() - } - cloudLogout() - } - } - - fun fetchMissingTransactions() { - if (opFetchTrns.value is OpResult.Loading) { - //wait for sync to finish - return - } - - if (opFetchTrns.value is OpResult.Success) { - //go to home screen - ivyContext.setMoreMenuExpanded(expanded = false) -// nav.navigateTo(Main) - return - } - - viewModelScope.launch { - _opFetchtrns.value = OpResult.loading() - - when (val res = fetchAllTrnsFromServerAct(Unit)) { - is Res.Ok -> _opFetchtrns.value = OpResult.success(Unit) - is Res.Err -> _opFetchtrns.value = OpResult.failure(res.error) + is SettingsEvent.AppLocked -> { + writeAppLockedAct(event.appLocked) } } } diff --git a/sync/base/.gitignore b/sync/base/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/sync/base/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/sync/base/src/main/AndroidManifest.xml b/sync/base/src/main/AndroidManifest.xml deleted file mode 100644 index 91f6bcb9a0..0000000000 --- a/sync/base/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/sync/base/src/main/java/com/ivy/sync/base/SyncItem.kt b/sync/base/src/main/java/com/ivy/sync/base/SyncItem.kt deleted file mode 100644 index 262f19a2ce..0000000000 --- a/sync/base/src/main/java/com/ivy/sync/base/SyncItem.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.ivy.sync.base - -import arrow.core.Either - -interface SyncItem { - - /** - * Provides an instance of the SyncItem only if it's **enabled**. - * @return **enabled SyncItem** instance or **null**. - */ - suspend fun enabled(): SyncItem? - - /** - * Saves a list of items. - * @return a list of the **successfully saved items**. - */ - suspend fun save(items: List): List - - /** - * Deletes a list of items. - * @return a list of the **successfully deleted items**. - */ - suspend fun delete(items: List): List - - /** - * #WIP - */ - suspend fun get(): Either> - -} \ No newline at end of file diff --git a/sync/ivy-server/.gitignore b/sync/ivy-server/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/sync/ivy-server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/sync/ivy-server/build.gradle.kts b/sync/ivy-server/build.gradle.kts deleted file mode 100644 index 217885caa2..0000000000 --- a/sync/ivy-server/build.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` - `kotlin-android` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":temp-persistence")) - implementation(project(":network")) - implementation(project(":sync:base")) -} \ No newline at end of file diff --git a/sync/ivy-server/src/main/AndroidManifest.xml b/sync/ivy-server/src/main/AndroidManifest.xml deleted file mode 100644 index d7b00852f1..0000000000 --- a/sync/ivy-server/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/sync/public/.gitignore b/sync/public/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/sync/public/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/sync/public/src/main/java/com/ivy/sync/SyncTask.kt b/sync/public/src/main/java/com/ivy/sync/SyncTask.kt deleted file mode 100644 index e55f05a811..0000000000 --- a/sync/public/src/main/java/com/ivy/sync/SyncTask.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.ivy.sync - -interface SyncTask { - suspend fun sync() -} - -fun syncTaskFrom( - f: suspend () -> Unit -): SyncTask = object : SyncTask { - override suspend fun sync() { - f() - } - -} \ No newline at end of file diff --git a/sync/public/src/main/java/com/ivy/sync/account/SyncAccountsAct.kt b/sync/public/src/main/java/com/ivy/sync/account/SyncAccountsAct.kt deleted file mode 100644 index 88e554265c..0000000000 --- a/sync/public/src/main/java/com/ivy/sync/account/SyncAccountsAct.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.ivy.sync.account -/* - -import com.ivy.data.account.Account -import com.ivy.frp.action.FPAction -import com.ivy.sync.base.SyncItem -import com.ivy.sync.ivyserver.account.AccountIvyServerSync -import com.ivy.temp.persistence.IOEffect -import com.ivy.temp.persistence.mapToEntity -import javax.inject.Inject - -class SyncAccountsAct @Inject constructor( - private val ivyServerSync: AccountIvyServerSync -) : FPAction>, Unit>() { - override suspend fun IOEffect>.compose(): suspend () -> Unit = { - sync(this) - } - - private suspend fun sync(operation: IOEffect>) { - val sync = ivyServerSync.enabled() ?: return - - when (operation) { - is IOEffect.Delete -> delete(sync = sync, items = operation.item) - is IOEffect.Save -> sync.save(operation.item) - } - } - - private suspend fun delete(sync: SyncItem, items: List) { - sync.delete(items) - // delete all locally not matter the result - items.forEach { - transactionDao.deleteAllByAccountId(accountId = it.id) - accountDao.deleteById(it.id) - } - } - - private suspend fun save(sync: SyncItem, items: List) = - sync.save(items).map { - val syncedItem = it.mark( - isSynced = true, isDeleted = false - ) - persist(syncedItem) - } - - private suspend fun persist(item: Account) { - accountDao.save(mapToEntity(item)) - } -}*/ diff --git a/sync/public/src/main/java/com/ivy/sync/category/SyncCategoriesAct.kt b/sync/public/src/main/java/com/ivy/sync/category/SyncCategoriesAct.kt deleted file mode 100644 index f361f8fc1b..0000000000 --- a/sync/public/src/main/java/com/ivy/sync/category/SyncCategoriesAct.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.ivy.sync.category -/* - -import com.ivy.data.SyncMetadata -import com.ivy.data.category.Category -import com.ivy.frp.action.FPAction -import com.ivy.sync.base.SyncItem -import com.ivy.sync.ivyserver.category.CategoryIvyServerSync -import com.ivy.temp.persistence.IOEffect -import com.ivy.temp.persistence.mapToEntity -import com.ivy.wallet.io.persistence.dao.CategoryDao -import javax.inject.Inject - -class SyncCategoriesAct @Inject constructor( - private val categoryDao: CategoryDao, private val ivyServerSync: CategoryIvyServerSync -) : FPAction>, Unit>() { - override suspend fun IOEffect>.compose(): suspend () -> Unit = { - sync(this) - } - - private suspend fun sync(operation: IOEffect>) { - val sync = ivyServerSync.enabled() ?: return - - when (operation) { - is IOEffect.Delete -> delete(sync, operation.item) - is IOEffect.Save -> save(sync, operation.item) - } - } - - private suspend fun delete(sync: SyncItem, items: List) { - sync.delete(items) - // delete all locally not matter the result - items.forEach { categoryDao.deleteById(it.id) } - } - - private suspend fun save(sync: SyncItem, items: List) = - sync.save(items) - .map { - val syncedItem = it.mark( - isSynced = true, isDeleted = false - ) - persist(syncedItem) - } - - private suspend fun persist(item: Category) { - categoryDao.save(mapToEntity(item)) - } -}*/ diff --git a/sync/public/src/main/java/com/ivy/sync/transaction/SyncTrnsAct.kt b/sync/public/src/main/java/com/ivy/sync/transaction/SyncTrnsAct.kt deleted file mode 100644 index 55a78cbef7..0000000000 --- a/sync/public/src/main/java/com/ivy/sync/transaction/SyncTrnsAct.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.ivy.sync.transaction -/* - -import com.ivy.data.SyncMetadata -import com.ivy.data.transaction.Transaction -import com.ivy.frp.action.FPAction -import com.ivy.sync.base.SyncItem -import com.ivy.sync.ivyserver.transaction.TrnIvyServerSync -import com.ivy.temp.persistence.IOEffect -import com.ivy.temp.persistence.mapToEntity -import com.ivy.wallet.io.persistence.dao.TransactionDao -import javax.inject.Inject - - -class SyncTrnsAct @Inject constructor( - private val transactionDao: TransactionDao, - private val ivyServerSync: TrnIvyServerSync, -) : FPAction>, Unit>() { - - override suspend fun IOEffect>.compose(): suspend () -> Unit = { - sync(this) - } - - private suspend fun sync(operation: IOEffect>) { - val sync = ivyServerSync.enabled() ?: return - - when (operation) { - is IOEffect.Delete -> delete(sync, operation.item) - is IOEffect.Save -> save(sync, operation.item) - } - } - - private suspend fun delete(sync: SyncItem, items: List) { - sync.delete(items) - // delete all locally not matter the result - items.forEach { transactionDao.deleteById(it.id) } - } - - private suspend fun save(sync: SyncItem, items: List) = - sync.save(items) - .map { - val syncedItem = it.mark( - isSynced = true, - isDeleted = false - ) - persist(syncedItem) - } - - - private suspend fun persist(item: Transaction) { - transactionDao.save(mapToEntity(item)) - } -}*/ diff --git a/temp-domain/.gitignore b/temp-domain/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/temp-domain/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/temp-domain/README.md b/temp-domain/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/temp-domain/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/temp-domain/build.gradle.kts b/temp-domain/build.gradle.kts deleted file mode 100644 index ab69c1c30a..0000000000 --- a/temp-domain/build.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -import com.ivy.buildsrc.Hilt -import com.ivy.buildsrc.ThirdParty - -apply() - -plugins { - `android-library` - `kotlin-android` -} - -dependencies { - Hilt() - implementation(project(":common:main")) - implementation(project(":core:data-model")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:exchange-provider")) - implementation(project(":common:main")) - implementation(project(":design-system")) - implementation(project(":navigation")) - ThirdParty() - - implementation(project(":temp-persistence")) - implementation(project(":temp-network")) - implementation(project(":android-notifications")) -} \ No newline at end of file diff --git a/temp-domain/src/main/AndroidManifest.xml b/temp-domain/src/main/AndroidManifest.xml deleted file mode 100644 index f30ae1fc93..0000000000 --- a/temp-domain/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/SmartTitleSuggestionsLogic.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/SmartTitleSuggestionsLogic.kt deleted file mode 100644 index fe0ecb2266..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/SmartTitleSuggestionsLogic.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic - -import com.ivy.data.transaction.TransactionOld -import com.ivy.wallet.utils.capitalizeWords - -@Deprecated("Use FP style, look into `domain.fp` package") -private fun List.extractUniqueTitles( - excludeSuggestions: Set? = null -): Set { - return this - .filter { !it.title.isNullOrBlank() } - .map { it.title!!.trim().capitalizeWords() } - .filter { excludeSuggestions == null || !excludeSuggestions.contains(it) } - .toSet() -} - -@Deprecated("Use FP style, look into `domain.fp` package") -private suspend fun Set.sortedByMostUsedFirst(countUses: suspend (String) -> Long): Set { - val titleCountMap = this - .map { - it to countUses(it) - } - .toMap() - - val sortedSuggestions = this - .sortedByDescending { - titleCountMap.getOrDefault(it, 0) - } - .toSet() - - return sortedSuggestions -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/CSVRow.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/CSVRow.kt deleted file mode 100644 index 1b926a8f93..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/CSVRow.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.csv.model - -data class CSVRow( - val index: Int, - val content: List -) \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/ImportApp.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/ImportApp.kt deleted file mode 100644 index d846f22593..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/ImportApp.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.csv.model - -import androidx.annotation.DrawableRes -import androidx.compose.ui.graphics.Color -import com.ivy.base.R -import com.ivy.design.l0_system.color.* - -enum class ImportApp { - IVY, - MONEY_MANAGER, - WALLET_BY_BUDGET_BAKERS, - SPENDEE, - MONEFY, - ONE_MONEY, - BLUE_COINS, - KTW_MONEY_MANAGER, - FORTUNE_CITY, - FINANCISTO, - MONEY_WALLET; - - fun color(): Color = when (this) { - IVY -> Purple - MONEY_MANAGER -> Red - WALLET_BY_BUDGET_BAKERS -> Green - SPENDEE -> RedLight - MONEFY -> Green - ONE_MONEY -> Red3 - BLUE_COINS -> Blue - KTW_MONEY_MANAGER -> Yellow - FORTUNE_CITY -> Green2Light - FINANCISTO -> White - MONEY_WALLET -> Blue2 - } - - fun appId(): String = when (this) { - IVY -> "com.ivy.wallet" - MONEY_MANAGER -> "com.realbyteapps.moneymanagerfree" - WALLET_BY_BUDGET_BAKERS -> "com.droid4you.application.wallet" - SPENDEE -> "com.cleevio.spendee" - MONEFY -> "com.monefy.app.lite" - ONE_MONEY -> "org.pixelrush.moneyiq" - BLUE_COINS -> "com.rammigsoftware.bluecoins" - KTW_MONEY_MANAGER -> "com.ktwapps.walletmanager" - FORTUNE_CITY -> "com.fourdesire.fortunecity" - FINANCISTO -> "ru.orangesoftware.financisto" - MONEY_WALLET -> "com.oriondev.moneywallet" - } - - @DrawableRes - fun logo(): Int = when (this) { - IVY -> R.drawable.ivywallet_logo - MONEY_MANAGER -> R.drawable.moneymanager_logo - WALLET_BY_BUDGET_BAKERS -> R.drawable.wallet_by_budgetbakers_logo - SPENDEE -> R.drawable.spendee_logo - MONEFY -> R.drawable.monefy_logo - ONE_MONEY -> R.drawable.one_money_logo - BLUE_COINS -> R.drawable.bluecoins - KTW_MONEY_MANAGER -> R.drawable.ktw_money_manager_logo - FORTUNE_CITY -> R.drawable.fortune_city_app_logo - FINANCISTO -> R.drawable.financisto_logo - MONEY_WALLET -> R.drawable.moneywallet_logo - } - - fun listName(): String = when (this) { - IVY -> "Ivy Wallet" - MONEY_MANAGER -> "Money Manager" - WALLET_BY_BUDGET_BAKERS -> "Wallet by BudgetBakers" - SPENDEE -> "Spendee" - MONEFY -> "Monefy" - ONE_MONEY -> "1Money" - BLUE_COINS -> "Bluecoins Finance" - KTW_MONEY_MANAGER -> "Money Manager (KTW)" - FORTUNE_CITY -> "Fortune City" - FINANCISTO -> "Financisto" - MONEY_WALLET -> "MoneyWallet" - } - - fun appName(): String = when (this) { - IVY -> "Ivy Wallet" - else -> listName() - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/ImportResult.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/ImportResult.kt deleted file mode 100644 index 681e3a2f37..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/ImportResult.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.csv.model - -data class ImportResult( - val rowsFound: Int, - val transactionsImported: Int, - val accountsImported: Int, - val categoriesImported: Int, - val failedRows: List, -) \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/RowMapping.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/RowMapping.kt deleted file mode 100644 index 7329952cd2..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/csv/model/RowMapping.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.csv.model - -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld - -data class RowMapping( - val type: Int? = null, - val defaultTypeToExpense: Boolean = false, - val amount: Int, - - val account: Int, - val accountCurrency: Int? = null, - val accountOrderNum: Int? = null, - val accountColor: Int? = null, - val accountIcon: Int? = null, - - val date: Int, - val dateOnlyFormat: String? = null, - val dateTimeFormat: String? = null, - val timeOnly: Int? = null, - val dueDate: Int? = null, - - val transferAmount: Int? = null, - val toAccount: Int? = null, - val toAmount: Int? = null, - val toAccountCurrency: Int? = null, - val toAccountColor: Int? = null, - val toAccountOrderNum: Int? = null, - val toAccountIcon: Int? = null, - - val category: Int?, - val categoryOrderNum: Int? = null, - val categoryColor: Int? = null, - val categoryIcon: Int? = null, - - val title: Int?, - val description: Int? = null, - val id: Int? = null, - - /** - * @param transaction - the final mapped transaction - * @param category - category object because Transaction#categoryId but no Category - * @param csvAmount - the original amount from the CSV file (can be negative, too) - */ - val transformTransaction: (TransactionOld, CategoryOld?, csvAmount: Double) -> TransactionOld = - { transaction, _, _ -> - transaction - }, - - val joinTransactions: (List) -> JoinResult = { transactions -> - JoinResult(transactions = transactions, mergedCount = 0) - } -) - -data class JoinResult( - val transactions: List, - val mergedCount: Int -) \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/notification/TransactionReminderLogic.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/notification/TransactionReminderLogic.kt deleted file mode 100644 index 1f12b7549b..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/notification/TransactionReminderLogic.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.notification - -import android.content.Context -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.io.persistence.SharedPrefs -import java.util.concurrent.TimeUnit - -@Deprecated("Use FP style, look into `domain.fp` package") -class TransactionReminderLogic( - private val appContext: Context, - private val sharedPrefs: SharedPrefs, -) { - companion object { - private const val UNIQUE_WORK_NAME_V1 = "transaction_reminder_work" - private const val UNIQUE_WORK_NAME_V2 = "transaction_reminder_work_v2" - private const val UNIQUE_WORK_NAME_TEST = "transaction_reminder_work_test" - } - - fun testNow() { - val workBuilder = PeriodicWorkRequestBuilder(5, TimeUnit.MINUTES) - - WorkManager - .getInstance(appContext) - .enqueueUniquePeriodicWork( - UNIQUE_WORK_NAME_TEST, - ExistingPeriodicWorkPolicy.REPLACE, - workBuilder.build() - ) - } - - fun scheduleReminder() { - if (!fetchShowNotifications()) - return - - val timeNowLocal = timeNow() - val today8PM = timeNow() - .withHour(20) - .withMinute(0) - - val initialDelaySeconds = if (today8PM.isAfter(timeNowLocal)) { - //8 PM is in the future, we can start reminder today - today8PM.toEpochSeconds(deviceTimeProvider()) - timeNowLocal.toEpochSeconds( - deviceTimeProvider() - ) - } else { - //8 PM has passed, we'll start reminding from tomorrow - today8PM.plusDays(1).toEpochSeconds(deviceTimeProvider()) - timeNowLocal.toEpochSeconds( - deviceTimeProvider() - ) - } - - val workBuilder = PeriodicWorkRequestBuilder(24, TimeUnit.HOURS) - if (initialDelaySeconds > 0) { - workBuilder.setInitialDelay(initialDelaySeconds, TimeUnit.SECONDS) - } - - WorkManager - .getInstance(appContext) - .enqueueUniquePeriodicWork( - UNIQUE_WORK_NAME_V2, - ExistingPeriodicWorkPolicy.KEEP, - workBuilder.build() - ) - } - - private fun fetchShowNotifications(): Boolean = - sharedPrefs.getBoolean(SharedPrefs.SHOW_NOTIFICATIONS, true) -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/notification/TransactionReminderWorker.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/notification/TransactionReminderWorker.kt deleted file mode 100644 index 3b7e095bb6..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/notification/TransactionReminderWorker.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.notification - -import android.app.PendingIntent -import android.content.Context -import androidx.core.app.NotificationCompat -import androidx.hilt.work.HiltWorker -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import com.ivy.base.R -import com.ivy.common.time.atEndOfDay -import com.ivy.common.time.dateNowUTC -import com.ivy.notifications.IvyNotificationChannel -import com.ivy.notifications.NotificationService -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.TransactionDao -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@HiltWorker -class TransactionReminderWorker @AssistedInject constructor( - @Assisted appContext: Context, @Assisted params: WorkerParameters, - private val transactionDao: TransactionDao, - private val notificationService: NotificationService, - private val sharedPrefs: SharedPrefs, -) : CoroutineWorker(appContext, params) { - - companion object { - const val MINIMUM_TRANSACTIONS_PER_DAY = 1 - } - - override suspend fun doWork() = withContext(Dispatchers.IO) { - - val transactionsToday = transactionDao.findAllBetween( - startDate = dateNowUTC().atStartOfDay(), - endDate = dateNowUTC().atEndOfDay() - ) - - val showNotifications = fetchShowNotifications() - - //Double check is needed because the user can switch off notifications in settings after it has been scheduled to show notifications for the next day - if (transactionsToday.size < MINIMUM_TRANSACTIONS_PER_DAY && showNotifications) { - //Have less than 1 two transactions today, remind them - - val notification = notificationService - .defaultIvyNotification( - channel = IvyNotificationChannel.TRANSACTION_REMINDER, - priority = NotificationCompat.PRIORITY_HIGH - ) - .setContentTitle("Ivy Wallet") - .setContentText(randomText()) - .setContentIntent( - PendingIntent.getActivity( - applicationContext, - 1, - com.ivy.core.ui.temp.GlobalProvider.rootIntent.getIntent(applicationContext), - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_IMMUTABLE - ) - ) - - notificationService.showNotification(notification, 1) - } - - return@withContext Result.success() - } - - private fun randomText(): String = - listOf( - com.ivy.core.ui.temp.stringRes(R.string.notification_1), - com.ivy.core.ui.temp.stringRes(R.string.notification_2), - com.ivy.core.ui.temp.stringRes(R.string.notification_3), - ).shuffled().first() - - private fun fetchShowNotifications(): Boolean = - sharedPrefs.getBoolean(SharedPrefs.SHOW_NOTIFICATIONS, true) -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/ExportZipLogic.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/ExportZipLogic.kt deleted file mode 100644 index c904326b32..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/ExportZipLogic.kt +++ /dev/null @@ -1,342 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.zip - -import android.content.Context -import android.net.Uri -import androidx.core.net.toUri -import com.google.gson.* -import com.google.gson.reflect.TypeToken -import com.ivy.base.readFile -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.toEpochMilli -import com.ivy.temp.deprecated.logic.zip.IvyWalletCompleteData -import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportResult -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.* -import com.ivy.wallet.utils.ioThread -import com.ivy.wallet.utils.scopedIOThread -import kotlinx.coroutines.async -import java.io.File -import java.lang.reflect.Type -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.util.* - - -class ExportZipLogic( - private val accountDao: AccountDao, - private val budgetDao: BudgetDao, - private val categoryDao: CategoryDao, - private val loanRecordDao: LoanRecordDao, - private val loanDao: LoanDao, - private val plannedPaymentRuleDao: PlannedPaymentRuleDao, - private val settingsDao: SettingsDao, - private val transactionDao: TransactionDao, - private val sharedPrefs: SharedPrefs, -) { - suspend fun exportToFile( - context: Context, - zipFileUri: Uri - ) { - val jsonString = generateJsonString() - val file = createJsonDataFile(context, jsonString) - zip(context = context, zipFileUri, listOf(file)) - clearCacheDir(context) - } - - private fun createJsonDataFile(context: Context, jsonString: String): File { - val fileNamePrefix = "data" - val fileNameSuffix = ".json" - val outputDir = context.cacheDir - - val file = File.createTempFile(fileNamePrefix, fileNameSuffix, outputDir) - file.writeText(jsonString, Charsets.UTF_16) - - return file - } - - private suspend fun generateJsonString(): String { - return scopedIOThread { - val accounts = it.async { accountDao.findAllSuspend() } - val budgets = it.async { budgetDao.findAll() } - val categories = it.async { categoryDao.findAllSuspend() } - val loanRecords = it.async { loanRecordDao.findAll() } - val loans = it.async { loanDao.findAll() } - val plannedPaymentRules = - it.async { plannedPaymentRuleDao.findAll() } - val settings = it.async { settingsDao.findAll() } - val transactions = it.async { transactionDao.findAll() } - val sharedPrefs = it.async { getSharedPrefsData() } - - val gson = GsonBuilder().registerTypeAdapter( - LocalDateTime::class.java, object : JsonSerializer { - @Throws(JsonParseException::class) - override fun serialize( - src: LocalDateTime?, - typeOfSrc: Type?, - context: JsonSerializationContext? - ): JsonElement { - return JsonPrimitive(src!!.toEpochMilli(deviceTimeProvider()).toString()) - } - }).create() - - val completeData = IvyWalletCompleteData( - accounts = accounts.await(), - budgets = budgets.await(), - categories = categories.await(), - loanRecords = loanRecords.await(), - loans = loans.await(), - plannedPaymentRules = plannedPaymentRules.await(), - settings = settings.await(), - transactions = transactions.await(), - sharedPrefs = sharedPrefs.await() - ) - - gson.toJson(completeData) - } - } - - private fun getSharedPrefsData(): HashMap { - val hashmap = HashMap() - hashmap[SharedPrefs.SHOW_NOTIFICATIONS] = - sharedPrefs.getBoolean(SharedPrefs.SHOW_NOTIFICATIONS, true).toString() - - hashmap[SharedPrefs.APP_LOCK_ENABLED] = - sharedPrefs.getBoolean(SharedPrefs.APP_LOCK_ENABLED, false).toString() - - hashmap[SharedPrefs.HIDE_CURRENT_BALANCE] = - sharedPrefs.getBoolean(SharedPrefs.HIDE_CURRENT_BALANCE, false).toString() - - hashmap[SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE] = - sharedPrefs.getBoolean(SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, false).toString() - - return hashmap - } - - suspend fun import( - context: Context, - zipFileUri: Uri, - onProgress: suspend (progressPercent: Double) -> Unit - ): ImportResult { - return ioThread { - return@ioThread try { - val folderName = "backup" + System.currentTimeMillis() - val cacheFolderPath = File(context.cacheDir, folderName) - - unzip(context, zipFileUri, cacheFolderPath) - - val filesArray = cacheFolderPath.listFiles() - - onProgress(0.05) - - if (filesArray == null || filesArray.isEmpty()) - ImportResult( - rowsFound = 0, - transactionsImported = 0, - accountsImported = 0, - categoriesImported = 0, - failedRows = emptyList() - ) - - val filesList = filesArray!!.toList().filter { - hasJsonExtension(it) - } - - onProgress(0.1) - - if (filesList.size != 1) - ImportResult( - rowsFound = 0, - transactionsImported = 0, - accountsImported = 0, - categoriesImported = 0, - failedRows = emptyList() - ) - - val jsonString = readFile(context, filesList[0].toUri(), Charsets.UTF_16) - val modifiedJsonString = accommodateExistingAccountsAndCategories(jsonString) - val ivyWalletCompleteData = getIvyWalletCompleteData(modifiedJsonString) - - onProgress(0.4) - insertDataToDb(completeData = ivyWalletCompleteData, onProgress = onProgress) - onProgress(1.0) - - clearCacheDir(context) - - ImportResult( - rowsFound = ivyWalletCompleteData.transactions.size, - transactionsImported = ivyWalletCompleteData.transactions.size, - accountsImported = ivyWalletCompleteData.accounts.size, - categoriesImported = ivyWalletCompleteData.categories.size, - failedRows = emptyList() - ) - - } catch (e: Exception) { - ImportResult( - rowsFound = 0, - transactionsImported = 0, - accountsImported = 0, - categoriesImported = 0, - failedRows = emptyList() - ) - } - } - } - - private suspend fun accommodateExistingAccountsAndCategories(jsonString: String?): String? { - val ivyWalletCompleteData = getIvyWalletCompleteData(jsonString) - val replacementPairs = getReplacementPairs(ivyWalletCompleteData) - - var modifiedString = jsonString - replacementPairs.forEach { - modifiedString = modifiedString!!.replace(it.first.toString(), it.second.toString()) - } - - return modifiedString - } - - private fun getIvyWalletCompleteData(data: String?): IvyWalletCompleteData { - val typeOfObjectsList: Type = - object : TypeToken() {}.type - - val gson: Gson = GsonBuilder().registerTypeAdapter( - LocalDateTime::class.java, object : JsonDeserializer { - @Throws(JsonParseException::class) - override fun deserialize( - json: JsonElement, - type: Type?, - jsonDeserializationContext: JsonDeserializationContext? - ): LocalDateTime? { - val instant: Instant = - Instant.ofEpochMilli(json.asJsonPrimitive.asLong) - return LocalDateTime.ofInstant(instant, ZoneOffset.UTC) - } - }).create() - - return gson.fromJson(data, typeOfObjectsList) - } - - private suspend fun insertDataToDb( - completeData: IvyWalletCompleteData, - onProgress: suspend (progressPercent: Double) -> Unit = {} - ) { - scopedIOThread { - transactionDao.save(completeData.transactions) - onProgress(0.6) - - val accounts = it.async { accountDao.save(completeData.accounts) } - val budgets = it.async { budgetDao.save(completeData.budgets) } - val categories = - it.async { categoryDao.save(completeData.categories) } - accounts.await() - budgets.await() - categories.await() - - onProgress(0.7) - - val loans = it.async { loanDao.save(completeData.loans) } - val loanRecords = - it.async { loanRecordDao.save(completeData.loanRecords) } - - loans.await() - loanRecords.await() - - onProgress(0.8) - - val plannedPayments = - it.async { plannedPaymentRuleDao.save(completeData.plannedPaymentRules) } - val settings = it.async { - settingsDao.deleteAll() - settingsDao.save(completeData.settings) - } - - sharedPrefs.putBoolean( - SharedPrefs.SHOW_NOTIFICATIONS, - (completeData.sharedPrefs[SharedPrefs.SHOW_NOTIFICATIONS] ?: "true").toBoolean() - ) - - sharedPrefs.putBoolean( - SharedPrefs.APP_LOCK_ENABLED, - (completeData.sharedPrefs[SharedPrefs.APP_LOCK_ENABLED] ?: "false").toBoolean() - ) - - sharedPrefs.putBoolean( - SharedPrefs.HIDE_CURRENT_BALANCE, - (completeData.sharedPrefs[SharedPrefs.HIDE_CURRENT_BALANCE] ?: "false").toBoolean() - ) - - sharedPrefs.putBoolean( - SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, - (completeData.sharedPrefs[SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE] - ?: "false").toBoolean() - ) - - plannedPayments.await() - settings.await() - - onProgress(0.9) - } - } - - /** This is used to replace account & category Ids in backup data with existing Ids - * This removes the problem of duplicate Accounts & Categories - * - * returns a Pair of IDs where A is the UUID that needs to be replaced with B - */ - private suspend fun getReplacementPairs( - completeData: IvyWalletCompleteData - ): List> { - return scopedIOThread { scope -> - val existingAccountsList = accountDao.findAllSuspend() - val existingCategoryList = categoryDao.findAllSuspend() - - val backupAccountsList = completeData.accounts - val backupCategoryList = completeData.categories - - if (existingAccountsList.isEmpty() && existingCategoryList.isEmpty()) - return@scopedIOThread emptyList() - - val sumAccountList = existingAccountsList + backupAccountsList - val sumCategoriesList = existingCategoryList + backupCategoryList - - val accountsReplace = scope.async { - sumAccountList.groupBy { it.name }.filter { it.value.size == 2 }.map { - val accountsZero = it.value[0] - val accountsFirst = it.value[1] - - if (backupAccountsList.contains(accountsZero)) - Pair(accountsZero.id, accountsFirst.id) - else - Pair(accountsFirst.id, accountsZero.id) - } - } - - val categoriesReplace = scope.async { - sumCategoriesList.groupBy { it.name }.filter { it.value.size == 2 }.map { - val categoryZero = it.value[0] - val categoryFirst = it.value[1] - - if (completeData.categories.contains(categoryZero)) - Pair(categoryZero.id, categoryFirst.id) - else - Pair(categoryFirst.id, categoryZero.id) - } - } - - return@scopedIOThread accountsReplace.await() + categoriesReplace.await() - } - } - - private fun hasJsonExtension(file: File): Boolean { - val name = file.name - val lastIndexOf = name.lastIndexOf(".") - if (lastIndexOf == -1) - return false - - return (name.substring(lastIndexOf).equals(".json", true)) - } - - private fun clearCacheDir(context: Context) { - context.cacheDir.deleteRecursively() - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/IvyWalletCompleteData.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/IvyWalletCompleteData.kt deleted file mode 100644 index 2103997096..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/IvyWalletCompleteData.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.ivy.temp.deprecated.logic.zip - -import com.ivy.wallet.io.persistence.data.* - -data class IvyWalletCompleteData( - val accounts: List = emptyList(), - val budgets: List = emptyList(), - val categories: List = emptyList(), - val loanRecords: List = emptyList(), - val loans: List = emptyList(), - val plannedPaymentRules: List = emptyList(), - val settings: List = emptyList(), - val transactions: List = emptyList(), - val sharedPrefs: HashMap = HashMap() -) \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/ZipUtils.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/ZipUtils.kt deleted file mode 100644 index 8b35ba1015..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/logic/zip/ZipUtils.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.ivy.wallet.domain.deprecated.logic.zip - -import android.content.Context -import android.net.Uri -import java.io.* -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream - -private const val MODE_WRITE = "w" -private const val MODE_READ = "r" - -fun zip(zipFile: File, files: List) { - ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { outStream -> - zip(outStream, files) - } -} - -fun zip(context: Context, zipFile: Uri, files: List) { - context.contentResolver.openFileDescriptor(zipFile, MODE_WRITE).use { descriptor -> - descriptor?.fileDescriptor?.let { - ZipOutputStream(BufferedOutputStream(FileOutputStream(it))).use { outStream -> - zip(outStream, files) - } - } - } -} - -private fun zip( - outStream: ZipOutputStream, - files: List, - includeParentFolder: Boolean = false -) { - files.forEach { file -> - if (file.isDirectory) { - file.mkdir() - zip(outStream, file.listFiles()?.toList() ?: emptyList(), includeParentFolder = true) - } else { - val fileLoc: String = - if (file.parent.isNullOrEmpty() || !includeParentFolder) file.name else (file.parent!!).substring( - file.parent!!.lastIndexOf("/") - ) + "/" + file.name - - outStream.putNextEntry(ZipEntry(fileLoc)) - BufferedInputStream(FileInputStream(file)).use { inStream -> - inStream.copyTo(outStream) - } - } - - } -} - -fun unzip(zipFile: File, location: File) { - ZipInputStream(BufferedInputStream(FileInputStream(zipFile))).use { inStream -> - unzip(inStream, location) - } -} - -fun unzip(context: Context, zipFile: Uri, location: File) { - context.contentResolver.openFileDescriptor(zipFile, MODE_READ).use { descriptor -> - descriptor?.fileDescriptor?.let { - ZipInputStream(BufferedInputStream(FileInputStream(it))).use { inStream -> - unzip(inStream, location) - } - } - } -} - -private fun unzip(inStream: ZipInputStream, location: File) { - if (location.exists() && !location.isDirectory) - throw IllegalStateException("Location file must be directory or not exist") - - if (!location.isDirectory) location.mkdirs() - - val locationPath = location.absolutePath.let { - if (!it.endsWith(File.separator)) "$it${File.separator}" - else it - } - - var zipEntry: ZipEntry? - var unzipFile: File - var unzipParentDir: File? - - while (inStream.nextEntry.also { zipEntry = it } != null) { - unzipFile = File(locationPath + zipEntry!!.name) - if (zipEntry!!.isDirectory) { - if (!unzipFile.isDirectory) unzipFile.mkdirs() - } else { - unzipParentDir = unzipFile.parentFile - if (unzipParentDir != null && !unzipParentDir.isDirectory) { - unzipParentDir.mkdirs() - } - BufferedOutputStream(FileOutputStream(unzipFile)).use { outStream -> - inStream.copyTo(outStream) - } - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/IvySync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/IvySync.kt deleted file mode 100644 index 6e6d1588c7..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/IvySync.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync - -import com.ivy.wallet.domain.deprecated.sync.item.* -import com.ivy.wallet.io.network.IvySession - -class IvySync( - private val accountSync: AccountSync, - private val categorySync: CategorySync, - private val budgetSync: BudgetSync, - private val transactionSync: TransactionSync, - private val plannedPaymentSync: PlannedPaymentSync, - private val loanSync: LoanSync, - private val loanRecordSync: LoanRecordSync, - private val ivySession: IvySession -) { - suspend fun isSynced(): Boolean { - return accountSync.isSynced() && - categorySync.isSynced() && - transactionSync.isSynced() && - plannedPaymentSync.isSynced() && - budgetSync.isSynced() && - loanSync.isSynced() && - loanRecordSync.isSynced() - } - - suspend fun sync() { - if (ivySession.isLoggedIn()) { - accountSync.sync() - categorySync.sync() - transactionSync.sync() - plannedPaymentSync.sync() - budgetSync.sync() - loanSync.sync() - loanRecordSync.sync() - } - } - - suspend fun syncCategories() { - categorySync.sync() - } - - suspend fun syncAccounts() { - accountSync.sync() - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/AccountSync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/AccountSync.kt deleted file mode 100644 index aea6089b6e..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/AccountSync.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.item - -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.domain.deprecated.sync.uploader.AccountUploader -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.AccountDao - -class AccountSync( - private val sharedPrefs: SharedPrefs, - private val dao: AccountDao, - restClient: RestClient, - private val uploader: AccountUploader, - private val ivySession: IvySession -) { - private val service = restClient.accountService - - suspend fun isSynced(): Boolean = - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = false).isEmpty() && - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = true).isEmpty() - - suspend fun sync() { - if (!ivySession.isLoggedIn()) return - - val syncStart = timeNow().toEpochSeconds(deviceTimeProvider()) - - uploadUpdated() - deleteDeleted() - fetchNew() - - sharedPrefs.putLong(SharedPrefs.LAST_SYNC_DATE_ACCOUNTS, syncStart) - } - - private suspend fun uploadUpdated() { - val toSync = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = false - ) - - for (item in toSync) { - uploader.sync(item.toDomain()) - } - } - - private suspend fun deleteDeleted() { - val toDelete = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = true - ) - - for (item in toDelete) { - uploader.delete(item.id) - } - } - - private suspend fun fetchNew() { - try { - val afterTimestamp = sharedPrefs.getEpochSeconds(SharedPrefs.LAST_SYNC_DATE_ACCOUNTS) - - val response = service.get(after = afterTimestamp) - - response.accounts.forEach { item -> - dao.save( - item.toEntity().copy( - isSynced = true, - isDeleted = false - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/BudgetSync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/BudgetSync.kt deleted file mode 100644 index 4492d25ea1..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/BudgetSync.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.item - -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.domain.deprecated.sync.uploader.BudgetUploader -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.BudgetDao - -class BudgetSync( - private val sharedPrefs: SharedPrefs, - private val dao: BudgetDao, - restClient: RestClient, - private val uploader: BudgetUploader, - private val ivySession: IvySession -) { - private val service = restClient.budgetService - - suspend fun isSynced(): Boolean = - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = false).isEmpty() && - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = true).isEmpty() - - suspend fun sync() { - if (!ivySession.isLoggedIn()) return - - val syncStart = timeNow().toEpochSeconds(deviceTimeProvider()) - - uploadUpdated() - deleteDeleted() - fetchNew() - - sharedPrefs.putLong(SharedPrefs.LAST_SYNC_DATE_BUDGETS, syncStart) - } - - private suspend fun uploadUpdated() { - val toSync = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = false - ) - - for (item in toSync) { - uploader.sync(item.toDomain()) - } - } - - private suspend fun deleteDeleted() { - val toDelete = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = true - ) - - for (item in toDelete) { - uploader.delete(item.id) - } - } - - private suspend fun fetchNew() { - try { - val afterTimestamp = sharedPrefs.getEpochSeconds(SharedPrefs.LAST_SYNC_DATE_BUDGETS) - - val response = service.get(after = afterTimestamp) - - response.budgets.forEach { item -> - dao.save( - item.toEntity().copy( - isSynced = true, - isDeleted = false - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/CategorySync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/CategorySync.kt deleted file mode 100644 index 897b5f4b5a..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/CategorySync.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.item - -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.domain.deprecated.sync.uploader.CategoryUploader -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.CategoryDao - -class CategorySync( - private val sharedPrefs: SharedPrefs, - private val dao: CategoryDao, - restClient: RestClient, - private val uploader: CategoryUploader, - private val ivySession: IvySession -) { - private val service = restClient.categoryService - - suspend fun isSynced(): Boolean = - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = false).isEmpty() && - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = true).isEmpty() - - suspend fun sync() { - if (!ivySession.isLoggedIn()) return - - val syncStart = timeNow().toEpochSeconds(deviceTimeProvider()) - - uploadUpdated() - deleteDeleted() - fetchNew() - - sharedPrefs.putLong(SharedPrefs.LAST_SYNC_DATE_CATEGORIES, syncStart) - } - - private suspend fun uploadUpdated() { - val toSync = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = false - ) - - for (item in toSync) { - uploader.sync(item.toDomain()) - } - } - - private suspend fun deleteDeleted() { - val toDelete = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = true - ) - - for (item in toDelete) { - uploader.delete(item.id) - } - } - - private suspend fun fetchNew() { - try { - val afterTimestamp = sharedPrefs.getEpochSeconds(SharedPrefs.LAST_SYNC_DATE_CATEGORIES) - - val response = service.get(after = afterTimestamp) - - response.categories.forEach { item -> - dao.save( - item.toEntity().copy( - isSynced = true, - isDeleted = false - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/LoanRecordSync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/LoanRecordSync.kt deleted file mode 100644 index f7410cb441..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/LoanRecordSync.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.item - -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.domain.deprecated.sync.uploader.LoanRecordUploader -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.LoanRecordDao - -class LoanRecordSync( - private val sharedPrefs: SharedPrefs, - private val dao: LoanRecordDao, - restClient: RestClient, - private val uploader: LoanRecordUploader, - private val ivySession: IvySession -) { - private val service = restClient.loanService - - suspend fun isSynced(): Boolean = - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = false).isEmpty() && - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = true).isEmpty() - - suspend fun sync() { - if (!ivySession.isLoggedIn()) return - - val syncStart = timeNow().toEpochSeconds(deviceTimeProvider()) - - uploadUpdated() - deleteDeleted() - fetchNew() - - sharedPrefs.putLong(SharedPrefs.LAST_SYNC_DATE_LOAN_RECORDS, syncStart) - } - - private suspend fun uploadUpdated() { - val toSync = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = false - ) - - for (item in toSync) { - uploader.sync(item.toDomain()) - } - } - - private suspend fun deleteDeleted() { - val toDelete = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = true - ) - - for (item in toDelete) { - uploader.delete(item.id) - } - } - - private suspend fun fetchNew() { - try { - val afterTimestamp = - sharedPrefs.getEpochSeconds(SharedPrefs.LAST_SYNC_DATE_LOAN_RECORDS) - - val response = service.getRecords(after = afterTimestamp) - - response.loanRecords.forEach { item -> - dao.save( - item.toEntity().copy( - isSynced = true, - isDeleted = false - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/LoanSync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/LoanSync.kt deleted file mode 100644 index c7e79b9dc6..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/LoanSync.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.item - -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.domain.deprecated.sync.uploader.LoanUploader -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.LoanDao - -class LoanSync( - private val sharedPrefs: SharedPrefs, - private val dao: LoanDao, - restClient: RestClient, - private val uploader: LoanUploader, - private val ivySession: IvySession -) { - private val service = restClient.loanService - - suspend fun isSynced(): Boolean = - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = false).isEmpty() && - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = true).isEmpty() - - suspend fun sync() { - if (!ivySession.isLoggedIn()) return - - val syncStart = timeNow().toEpochSeconds(deviceTimeProvider()) - - uploadUpdated() - deleteDeleted() - fetchNew() - - sharedPrefs.putLong(SharedPrefs.LAST_SYNC_DATE_LOANS, syncStart) - } - - private suspend fun uploadUpdated() { - val toSync = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = false - ) - - for (item in toSync) { - uploader.sync(item.toDomain()) - } - } - - private suspend fun deleteDeleted() { - val toDelete = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = true - ) - - for (item in toDelete) { - uploader.delete(item.id) - } - } - - private suspend fun fetchNew() { - try { - val afterTimestamp = sharedPrefs.getEpochSeconds(SharedPrefs.LAST_SYNC_DATE_LOANS) - - val response = service.get(after = afterTimestamp) - - response.loans.forEach { item -> - dao.save( - item.toEntity().copy( - isSynced = true, - isDeleted = false - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/PlannedPaymentSync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/PlannedPaymentSync.kt deleted file mode 100644 index ea746fe888..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/PlannedPaymentSync.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.item - -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.domain.deprecated.sync.uploader.PlannedPaymentRuleUploader -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.PlannedPaymentRuleDao - -class PlannedPaymentSync( - private val sharedPrefs: SharedPrefs, - private val dao: PlannedPaymentRuleDao, - restClient: RestClient, - private val uploader: PlannedPaymentRuleUploader, - private val ivySession: IvySession -) { - private val service = restClient.plannedPaymentRuleService - - suspend fun isSynced(): Boolean = - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = false).isEmpty() && - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = true).isEmpty() - - suspend fun sync() { - if (!ivySession.isLoggedIn()) return - - - val syncStart = timeNow().toEpochSeconds(deviceTimeProvider()) - - uploadUpdated() - deleteDeleted() - fetchNew() - - sharedPrefs.putLong(SharedPrefs.LAST_SYNC_DATE_PLANNED_PAYMENTS, syncStart) - } - - private suspend fun uploadUpdated() { - val toSync = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = false - ) - - for (item in toSync) { - uploader.sync(item.toDomain()) - } - } - - private suspend fun deleteDeleted() { - val toDelete = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = true - ) - - for (item in toDelete) { - uploader.delete(item.id) - } - } - - private suspend fun fetchNew() { - try { - val afterTimestamp = - sharedPrefs.getEpochSeconds(SharedPrefs.LAST_SYNC_DATE_PLANNED_PAYMENTS) - - val response = service.get(after = afterTimestamp) - - response.rules.forEach { item -> - dao.save( - item.toEntity().copy( - isSynced = true, - isDeleted = false - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/TransactionSync.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/TransactionSync.kt deleted file mode 100644 index a2e306cf05..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/item/TransactionSync.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.item - -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.timeNow -import com.ivy.common.time.toEpochSeconds -import com.ivy.wallet.domain.deprecated.sync.uploader.TransactionUploader -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.TransactionDao - -class TransactionSync( - private val sharedPrefs: SharedPrefs, - private val dao: TransactionDao, - restClient: RestClient, - private val uploader: TransactionUploader, - private val ivySession: IvySession -) { - private val service = restClient.transactionService - - suspend fun isSynced(): Boolean = - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = false).isEmpty() && - dao.findByIsSyncedAndIsDeleted(synced = false, deleted = true).isEmpty() - - suspend fun sync() { - if (!ivySession.isLoggedIn()) return - - val syncStart = timeNow().toEpochSeconds(deviceTimeProvider()) - - uploadUpdated() - deleteDeleted() - fetchNew() - - sharedPrefs.putLong(SharedPrefs.LAST_SYNC_DATE_TRANSACTIONS, syncStart) - } - - private suspend fun uploadUpdated() { - val toSync = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = false - ) - - for (item in toSync) { - uploader.sync(item.toDomain()) - } - } - - private suspend fun deleteDeleted() { - val toDelete = dao.findByIsSyncedAndIsDeleted( - synced = false, - deleted = true - ) - - for (item in toDelete) { - uploader.delete(item.id) - } - } - - private suspend fun fetchNew() { - try { - val afterTimestamp = - sharedPrefs.getEpochSeconds(SharedPrefs.LAST_SYNC_DATE_TRANSACTIONS) - - val response = service.get(after = afterTimestamp) - - response.transactions.forEach { item -> - dao.save( - item.toEntity().copy( - isSynced = true, - isDeleted = false - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/AccountUploader.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/AccountUploader.kt deleted file mode 100644 index 3948968250..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/AccountUploader.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.uploader - -import com.ivy.data.AccountOld -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.data.toDTO -import com.ivy.wallet.io.network.request.account.DeleteAccountRequest -import com.ivy.wallet.io.network.request.account.UpdateAccountRequest -import com.ivy.wallet.io.persistence.dao.AccountDao -import com.ivy.wallet.io.persistence.dao.TransactionDao -import com.ivy.wallet.io.persistence.data.toEntity -import timber.log.Timber -import java.util.* - -class AccountUploader( - private val accountDao: AccountDao, - private val transactionDao: TransactionDao, - restClient: RestClient, - private val ivySession: IvySession -) { - private val service = restClient.accountService - - suspend fun sync(item: AccountOld) { - if (!ivySession.isLoggedIn()) return - - try { - //update - service.update( - UpdateAccountRequest( - account = item.toDTO() - ) - ) - - //flag as synced - accountDao.save( - item.copy( - isSynced = true - ).toEntity() - ) - Timber.d("Account updated: $item.") - } catch (e: Exception) { - Timber.e("Failed to update with error (${e.message}): $item") - e.printStackTrace() - } - } - - - suspend fun delete(id: UUID) { - if (!ivySession.isLoggedIn()) return - - try { - //Delete on server - service.delete( - DeleteAccountRequest( - id = id - ) - ) - - //delete from local db - transactionDao.deleteAllByAccountId(id) - accountDao.deleteById(id) - Timber.d("Account deleted: $id.") - } catch (e: Exception) { - Timber.e("Failed to delete with error (${e.message}): $id") - e.printStackTrace() - - //delete from local db - transactionDao.deleteAllByAccountId(id) - accountDao.deleteById(id) - } - } - -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/BudgetUploader.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/BudgetUploader.kt deleted file mode 100644 index ca653a5131..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/BudgetUploader.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.uploader - -import com.ivy.data.Budget -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.data.toDTO -import com.ivy.wallet.io.network.request.budget.CrupdateBudgetRequest -import com.ivy.wallet.io.network.request.budget.DeleteBudgetRequest -import com.ivy.wallet.io.persistence.dao.BudgetDao -import com.ivy.wallet.io.persistence.data.toEntity -import timber.log.Timber -import java.util.* - -class BudgetUploader( - private val dao: BudgetDao, - restClient: RestClient, - private val ivySession: IvySession -) { - private val service = restClient.budgetService - - suspend fun sync(item: Budget) { - if (!ivySession.isLoggedIn()) return - - try { - //update - service.update( - CrupdateBudgetRequest( - budget = item.toDTO() - ) - ) - - //flag as synced - dao.save( - item.copy( - isSynced = true - ).toEntity() - ) - Timber.d("Budget updated: $item.") - } catch (e: Exception) { - Timber.e("Failed to update with error (${e.message}): $item") - e.printStackTrace() - } - } - - - suspend fun delete(id: UUID) { - if (!ivySession.isLoggedIn()) return - - try { - //Delete on server - service.delete( - DeleteBudgetRequest( - id = id - ) - ) - - //delete from local db - dao.deleteById(id) - Timber.d("Budget deleted: $id.") - } catch (e: Exception) { - Timber.e("Failed to delete with error (${e.message}): $id") - e.printStackTrace() - - //delete from local db - dao.deleteById(id) - } - } - -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/CategoryUploader.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/CategoryUploader.kt deleted file mode 100644 index 722a14ca3a..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/CategoryUploader.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.uploader - -import com.ivy.data.CategoryOld -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.data.toDTO -import com.ivy.wallet.io.network.request.category.DeleteWalletCategoryRequest -import com.ivy.wallet.io.network.request.category.UpdateWalletCategoryRequest -import com.ivy.wallet.io.persistence.dao.CategoryDao -import com.ivy.wallet.io.persistence.data.toEntity -import timber.log.Timber -import java.util.* - -class CategoryUploader( - private val dao: CategoryDao, - restClient: RestClient, - private val ivySession: IvySession -) { - private val service = restClient.categoryService - - suspend fun sync(item: CategoryOld) { - if (!ivySession.isLoggedIn()) return - - try { - //update - service.update( - UpdateWalletCategoryRequest( - category = item.toDTO() - ) - ) - - //flag as synced - dao.save( - item.copy( - isSynced = true - ).toEntity() - ) - Timber.d("Category updated: $item.") - } catch (e: Exception) { - Timber.e("Failed to update with error (${e.message}): $item") - e.printStackTrace() - } - } - - - suspend fun delete(id: UUID) { - if (!ivySession.isLoggedIn()) return - - try { - //Delete on server - service.delete( - DeleteWalletCategoryRequest( - id = id - ) - ) - - //delete from local db - dao.deleteById(id) - Timber.d("Category deleted: $id.") - } catch (e: Exception) { - Timber.e("Failed to delete with error (${e.message}): $id") - e.printStackTrace() - - //delete from local db - dao.deleteById(id) - } - } - -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/LoanRecordUploader.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/LoanRecordUploader.kt deleted file mode 100644 index 7dd3188d7f..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/LoanRecordUploader.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.uploader - -import com.ivy.data.loan.LoanRecord -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.data.toDTO -import com.ivy.wallet.io.network.request.loan.DeleteLoanRecordRequest -import com.ivy.wallet.io.network.request.loan.UpdateLoanRecordRequest -import com.ivy.wallet.io.persistence.dao.LoanRecordDao -import com.ivy.wallet.io.persistence.data.toEntity -import timber.log.Timber -import java.util.* - -class LoanRecordUploader( - private val dao: LoanRecordDao, - restClient: RestClient, - private val ivySession: IvySession -) { - private val service = restClient.loanService - - suspend fun sync(item: LoanRecord) { - if (!ivySession.isLoggedIn()) return - - try { - //update - service.updateRecord( - UpdateLoanRecordRequest( - loanRecord = item.toDTO() - ) - ) - - //flag as synced - dao.save( - item.copy( - isSynced = true - ).toEntity() - ) - Timber.d("Loan record updated: $item.") - } catch (e: Exception) { - Timber.e("Failed to update with error (${e.message}): $item") - e.printStackTrace() - } - } - - - suspend fun delete(id: UUID) { - if (!ivySession.isLoggedIn()) return - - try { - //Delete on server - service.deleteRecord( - DeleteLoanRecordRequest( - id = id - ) - ) - - //delete from local db - dao.deleteById(id) - Timber.d("Loan record deleted: $id.") - } catch (e: Exception) { - Timber.e("Failed to delete with error (${e.message}): $id") - e.printStackTrace() - - //delete from local db - dao.deleteById(id) - } - } - -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/LoanUploader.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/LoanUploader.kt deleted file mode 100644 index 9278971aa9..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/LoanUploader.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.uploader - -import com.ivy.data.loan.Loan -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.data.toDTO -import com.ivy.wallet.io.network.request.loan.DeleteLoanRequest -import com.ivy.wallet.io.network.request.loan.UpdateLoanRequest -import com.ivy.wallet.io.persistence.dao.LoanDao -import com.ivy.wallet.io.persistence.data.toEntity -import timber.log.Timber -import java.util.* - -class LoanUploader( - private val dao: LoanDao, - restClient: RestClient, - private val ivySession: IvySession -) { - private val service = restClient.loanService - - suspend fun sync(item: Loan) { - if (!ivySession.isLoggedIn()) return - - try { - //update - service.update( - UpdateLoanRequest( - loan = item.toDTO() - ) - ) - - //flag as synced - dao.save( - item.copy( - isSynced = true - ).toEntity() - ) - Timber.d("Loan updated: $item.") - } catch (e: Exception) { - Timber.e("Failed to update with error (${e.message}): $item") - e.printStackTrace() - } - } - - - suspend fun delete(id: UUID) { - if (!ivySession.isLoggedIn()) return - - try { - //Delete on server - service.delete( - DeleteLoanRequest( - id = id - ) - ) - - //delete from local db - dao.deleteById(id) - Timber.d("Loan deleted: $id.") - } catch (e: Exception) { - Timber.e("Failed to delete with error (${e.message}): $id") - e.printStackTrace() - - //delete from local db - dao.deleteById(id) - } - } - -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/PlannedPaymentRuleUploader.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/PlannedPaymentRuleUploader.kt deleted file mode 100644 index feb228d15d..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/PlannedPaymentRuleUploader.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.uploader - -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.data.toDTO -import com.ivy.wallet.io.network.request.planned.DeletePlannedPaymentRuleRequest -import com.ivy.wallet.io.network.request.planned.UpdatePlannedPaymentRuleRequest -import com.ivy.wallet.io.persistence.dao.PlannedPaymentRuleDao -import com.ivy.wallet.io.persistence.data.toEntity -import timber.log.Timber -import java.util.* - -class PlannedPaymentRuleUploader( - private val dao: PlannedPaymentRuleDao, - restClient: RestClient, - private val ivySession: IvySession -) { - private val service = restClient.plannedPaymentRuleService - - suspend fun sync(item: PlannedPaymentRule) { - if (!ivySession.isLoggedIn()) return - - try { - //update - service.update( - UpdatePlannedPaymentRuleRequest( - rule = item.toDTO() - ) - ) - - //flag as synced - dao.save( - item.copy( - isSynced = true - ).toEntity() - ) - Timber.d("PlannedPaymentRule updated: $item.") - } catch (e: Exception) { - Timber.e("Failed to update with error (${e.message}): $item") - e.printStackTrace() - } - } - - - suspend fun delete(id: UUID) { - if (!ivySession.isLoggedIn()) return - - try { - //Delete on server - service.delete( - DeletePlannedPaymentRuleRequest( - id = id - ) - ) - - //delete from local db - dao.deleteById(id) - Timber.d("PlannedPaymentRule deleted: $id.") - } catch (e: Exception) { - Timber.e("Failed to delete with error (${e.message}): $id") - e.printStackTrace() - - //delete from local db - dao.deleteById(id) - } - } - -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/TransactionUploader.kt b/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/TransactionUploader.kt deleted file mode 100644 index ba3f38620d..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/deprecated/sync/uploader/TransactionUploader.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.ivy.wallet.domain.deprecated.sync.uploader - -import com.ivy.data.transaction.TransactionOld -import com.ivy.wallet.io.network.IvySession -import com.ivy.wallet.io.network.RestClient -import com.ivy.wallet.io.network.data.toDTO -import com.ivy.wallet.io.network.request.transaction.DeleteTransactionRequest -import com.ivy.wallet.io.network.request.transaction.UpdateTransactionRequest -import com.ivy.wallet.io.persistence.dao.TransactionDao -import com.ivy.wallet.io.persistence.data.toEntity -import timber.log.Timber -import java.util.* - -class TransactionUploader( - private val dao: TransactionDao, - restClient: RestClient, - private val ivySession: IvySession -) { - private val service = restClient.transactionService - - suspend fun sync(item: TransactionOld) { - if (!ivySession.isLoggedIn()) return - - try { - //update - service.update( - UpdateTransactionRequest( - transaction = item.toDTO() - ) - ) - - //flag as synced - dao.save( - item.copy( - isSynced = true - ).toEntity() - ) - Timber.d("Transaction updated: $item.") - } catch (e: Exception) { - Timber.e("Failed to update with error (${e.message}): $item") - e.printStackTrace() - } - } - - - suspend fun delete(id: UUID) { - if (!ivySession.isLoggedIn()) return - - try { - //Delete on server - service.delete( - DeleteTransactionRequest( - id = id - ) - ) - - //delete from local db - dao.deleteById(id) - Timber.d("Transaction deleted: $id.") - } catch (e: Exception) { - Timber.e("Failed to delete with error (${e.message}): $id") - e.printStackTrace() - - //delete from local db - dao.deleteById(id) - } - } - -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/event/AccountsUpdatedEvent.kt b/temp-domain/src/main/java/com/ivy/temp/event/AccountsUpdatedEvent.kt deleted file mode 100644 index 7ffbed2ae4..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/event/AccountsUpdatedEvent.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.ivy.temp.event - -class AccountsUpdatedEvent { -} \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/pure/account/AccountFunctions.kt b/temp-domain/src/main/java/com/ivy/temp/pure/account/AccountFunctions.kt deleted file mode 100644 index 4292b6ee36..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/pure/account/AccountFunctions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.ivy.wallet.domain.pure.account - -import com.ivy.data.AccountOld - -fun filterExcluded(accounts: List): List = - accounts.filter { it.includeInBalance } - -fun accountCurrency(account: AccountOld, baseCurrency: String): String = - account.currency ?: baseCurrency \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/pure/data/IncomeExpenseTransferPair.kt b/temp-domain/src/main/java/com/ivy/temp/pure/data/IncomeExpenseTransferPair.kt deleted file mode 100644 index 2301ccfba6..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/pure/data/IncomeExpenseTransferPair.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.ivy.wallet.domain.pure.data - -import java.math.BigDecimal - -data class IncomeExpenseTransferPair( - val income: BigDecimal, - val expense: BigDecimal, - val transferIncome: BigDecimal, - val transferExpense: BigDecimal -) { - companion object { - fun zero(): IncomeExpenseTransferPair = IncomeExpenseTransferPair( - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO - ) - } -} diff --git a/temp-domain/src/main/java/com/ivy/temp/pure/data/WalletDAOs.kt b/temp-domain/src/main/java/com/ivy/temp/pure/data/WalletDAOs.kt deleted file mode 100644 index b67717d8e4..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/pure/data/WalletDAOs.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ivy.wallet.domain.pure.data - -import com.ivy.temp.persistence.ExchangeRateDao -import com.ivy.wallet.io.persistence.dao.AccountDao -import com.ivy.wallet.io.persistence.dao.TransactionDao - -data class WalletDAOs( - val accountDao: AccountDao, - val transactionDao: TransactionDao, - val exchangeRateDao: ExchangeRateDao -) \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/pure/util/IvyDomainUtils.kt b/temp-domain/src/main/java/com/ivy/temp/pure/util/IvyDomainUtils.kt deleted file mode 100644 index 2b1eb487e9..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/pure/util/IvyDomainUtils.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.ivy.wallet.domain.pure.util - -fun Double?.nextOrderNum(): Double = this?.plus(1) ?: 0.0 \ No newline at end of file diff --git a/temp-domain/src/main/java/com/ivy/temp/pure/util/Utils.kt b/temp-domain/src/main/java/com/ivy/temp/pure/util/Utils.kt deleted file mode 100644 index 71c53d3627..0000000000 --- a/temp-domain/src/main/java/com/ivy/temp/pure/util/Utils.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.ivy.wallet.domain.pure.util - -import arrow.core.NonEmptyList -import arrow.core.Option -import java.math.BigDecimal - -fun NonEmptyList.mapIndexedNel( - f: (Int, T) -> T -): NonEmptyList { - return NonEmptyList.fromListUnsafe( - this.mapIndexed(f) - ) -} - -suspend fun NonEmptyList.mapIndexedNelSuspend( - f: suspend (Int, T) -> T -): NonEmptyList { - return NonEmptyList.fromListUnsafe( - this.mapIndexed { index, value -> - f(index, value) - } - ) -} - -fun nonEmptyListOfZeros(n: Int): NonEmptyList { - return NonEmptyList.fromListUnsafe( - List(n) { BigDecimal.ZERO } - ) -} - -fun Option.orZero(): BigDecimal { - return this.orNull() ?: BigDecimal.ZERO -} \ No newline at end of file diff --git a/temp-network/.gitignore b/temp-network/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/temp-network/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/temp-network/README.md b/temp-network/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/temp-network/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/temp-network/build.gradle.kts b/temp-network/build.gradle.kts deleted file mode 100644 index 6addb19f78..0000000000 --- a/temp-network/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -import com.ivy.buildsrc.Hilt -import com.ivy.buildsrc.Networking - -apply() - -plugins { - `android-library` - `kotlin-android` -} - -dependencies { - Hilt() - implementation(project(":common:main")) - implementation(project(":core:data-model")) - implementation(project(":temp-persistence")) - Networking(api = true) - -} \ No newline at end of file diff --git a/temp-network/src/main/AndroidManifest.xml b/temp-network/src/main/AndroidManifest.xml deleted file mode 100644 index 1f8685dd68..0000000000 --- a/temp-network/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/GsonTypeAdapters.kt b/temp-network/src/main/java/com/ivy/temp/network/GsonTypeAdapters.kt deleted file mode 100644 index 9c2a0c9855..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/GsonTypeAdapters.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.ivy.wallet.io.network - -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonWriter -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.toEpochSeconds -import com.ivy.common.time.toLocal -import com.ivy.wallet.io.network.error.ErrorCode -import java.time.Instant -import java.time.LocalDateTime - -class LocalDateTimeTypeAdapter : TypeAdapter() { - override fun write(out: JsonWriter, date: LocalDateTime?) { - date?.let { - out.value(it.toEpochSeconds(deviceTimeProvider())) - } ?: out.nullValue() - } - - override fun read(jsonIn: JsonReader): LocalDateTime? { - return try { - val timestampSeconds = jsonIn.nextLong() - Instant.ofEpochSecond(timestampSeconds).toLocal(deviceTimeProvider()) - } catch (e: Exception) { - jsonIn.nextNull() - null - } - } -} - -class ErrorCodeTypeAdapter : TypeAdapter() { - override fun write(out: JsonWriter, value: ErrorCode?) { - value?.let { - out.value(value.code) - } ?: out.nullValue() - } - - override fun read(jsonIn: JsonReader): ErrorCode { - return try { - val code = jsonIn.nextInt() - ErrorCode.values().find { it.code == code } ?: ErrorCode.UNKNOWN - } catch (e: Exception) { - jsonIn.nextNull() - ErrorCode.UNKNOWN - } - } - -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/IvySession.kt b/temp-network/src/main/java/com/ivy/temp/network/IvySession.kt deleted file mode 100644 index 5f3d2ddffb..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/IvySession.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.ivy.wallet.io.network - -import com.ivy.wallet.io.network.request.auth.AuthResponse -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.UserDao -import java.util.* - -class IvySession( - private val sharedPrefs: SharedPrefs, - private val userDao: UserDao -) { - private var userId: UUID? = null - private var authToken: String? = null - - fun loadFromCache() { - userId = sharedPrefs.getString(SharedPrefs.SESSION_USER_ID, null) - ?.let { UUID.fromString(it) } - authToken = sharedPrefs.getString(SharedPrefs.SESSION_AUTH_TOKEN, null) - } - - fun getSessionToken() = authToken ?: throw NoSessionException() - - fun getUserId(): UUID = userId ?: throw NoSessionException() - - fun getUserIdSafe(): UUID? = userId - - fun isLoggedIn(): Boolean { - return userId != null && authToken != null - } - - suspend fun initiate(authResponse: AuthResponse) { - val user = authResponse.user - userDao.save(user.toEntity()) - - sharedPrefs.putString(SharedPrefs.SESSION_USER_ID, user.id.toString()) - sharedPrefs.putString(SharedPrefs.SESSION_AUTH_TOKEN, authResponse.sessionToken) - - userId = authResponse.user.id - authToken = authResponse.sessionToken - } - - fun logout() { - sharedPrefs.remove(SharedPrefs.SESSION_USER_ID) - sharedPrefs.remove(SharedPrefs.SESSION_AUTH_TOKEN) - - userId = null - authToken = null - } -} - -class NoSessionException : IllegalStateException("No session.") \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/RestClient.kt b/temp-network/src/main/java/com/ivy/temp/network/RestClient.kt deleted file mode 100644 index 6e204a96dc..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/RestClient.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.ivy.wallet.io.network - -import android.annotation.SuppressLint -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import com.google.gson.Gson -import com.ivy.common.BuildConfig -import com.ivy.wallet.io.network.error.ErrorCode -import com.ivy.wallet.io.network.error.NetworkError -import com.ivy.wallet.io.network.error.RestError -import com.ivy.wallet.io.network.service.* -import okhttp3.Credentials -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import timber.log.Timber -import java.security.SecureRandom -import java.security.cert.CertificateException -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -class RestClient private constructor( - private val appContext: Context, - private val retrofit: Retrofit -) { - - companion object { - private const val API_URL = "https://ivy-apps.com" - private const val HEADER_USER_ID = "userId" - private const val HEADER_SESSION_TOKEN = "sessionToken" - - private var networkAvailable = false - - fun initialize(appContext: Context, session: IvySession, gson: Gson): RestClient { - val retrofit = newRetrofit(gson, session) - return RestClient(appContext, retrofit).apply { - monitorNetworkConnectivity() - } - } - - private fun newRetrofit(gson: Gson, session: IvySession): Retrofit { - val httpClientBuilder = OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .callTimeout(15, TimeUnit.SECONDS) - - if (BuildConfig.DEBUG) { - val loggingInterceptor = HttpLoggingInterceptor().apply { - setLevel(HttpLoggingInterceptor.Level.BODY) - } - - httpClientBuilder.addInterceptor(loggingInterceptor) - } - - - //Add AUTH headers - httpClientBuilder.addInterceptor { - try { - val request = it.request() - .newBuilder() - .addHeader(HEADER_USER_ID, session.getUserId().toString()) - .addHeader(HEADER_SESSION_TOKEN, session.getSessionToken()) - .build() - - it.proceed(request) - } catch (e: NoSessionException) { - //Session not initialized, yet - do nothing - it.proceed(it.request()) - } - } - - //Handle Server errors - httpClientBuilder.addInterceptor(Interceptor { chain -> - val response = chain.proceed(chain.request()) - - if (response.code < 200 || response.code > 299) { - response.body?.string()?.let { errorBody -> - try { - Timber.e("Server error: $errorBody") - val restError = gson.fromJson( - errorBody, - RestError::class.java - ) ?: RestError(ErrorCode.UNKNOWN, "Failed to parse RestError.") - throw NetworkError(restError) - } catch (exception: Exception) { - throw if (exception is NetworkError) exception else { - exception.printStackTrace() - NetworkError(RestError(ErrorCode.UNKNOWN, exception.message)) - } - } - } ?: throw NetworkError(RestError(ErrorCode.UNKNOWN, "Empty error body.")) - } - response - }) - - //Github Rest API interceptor (not the best solution) - httpClientBuilder.addInterceptor(Interceptor { chain -> - val request = chain.request() - val finalRequest = - if (request.url.toUrl().toString().startsWith(GithubService.BASE_URL)) { - val credentials = Credentials.basic( - GithubService.GITHUB_SERVICE_ACC_USERNAME, - GithubService.GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_1 + - GithubService.GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_2 - ) - - request.newBuilder() - .header("Authorization", credentials) - .build() - } else { - request - } - - chain.proceed(request = finalRequest) - }) - - trustAllSSLCertificates(httpClientBuilder) - - return Retrofit.Builder() - .baseUrl(API_URL) - .client(httpClientBuilder.build()) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build() - } - - @SuppressLint("TrustAllX509TrustManager") - fun trustAllSSLCertificates(okHttpBuilder: OkHttpClient.Builder) { - //TODO: SECURITY - Considering trusting only Ivy's cert - val trustAllCerts = arrayOf( - object : X509TrustManager { - @Throws(CertificateException::class) - override fun checkClientTrusted( - chain: Array?, - authType: String? - ) { - } - - @Throws(CertificateException::class) - override fun checkServerTrusted( - chain: Array?, - authType: String? - ) { - } - - override fun getAcceptedIssuers(): Array? { - return arrayOf() - } - } - ) - - // Install the all-trusting trust manager - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, trustAllCerts, SecureRandom()) - // Create an ssl socket factory with our all-trusting manager - // Create an ssl socket factory with our all-trusting manager - val sslSocketFactory = sslContext.socketFactory - - okHttpBuilder.sslSocketFactory(sslSocketFactory, (trustAllCerts[0] as X509TrustManager)) - okHttpBuilder.hostnameVerifier(HostnameVerifier { _, _ -> - true - }) - } - } - - private fun monitorNetworkConnectivity() { - val connectivityManager = - appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - connectivityManager.registerDefaultNetworkCallback(object : - ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - networkAvailable = true - Timber.d("Network available: $network. (networkAvailable = $networkAvailable)") - } - - override fun onLost(network: Network) { - networkAvailable = false - Timber.d("Network lost: $network. (networkAvailable = $networkAvailable)") - } - - override fun onUnavailable() { - networkAvailable = false - Timber.d("Network unavailable. (networkAvailable = $networkAvailable)") - } - }) - } - - val authService: AuthService by lazy { retrofit.create(AuthService::class.java) } - val categoryService: CategoryService by lazy { retrofit.create(CategoryService::class.java) } - val accountService: AccountService by lazy { retrofit.create(AccountService::class.java) } - val budgetService: BudgetService by lazy { retrofit.create(BudgetService::class.java) } - val loanService: LoanService by lazy { retrofit.create(LoanService::class.java) } - val transactionService: TransactionService by lazy { retrofit.create(TransactionService::class.java) } - val plannedPaymentRuleService: PlannedPaymentRuleService by lazy { - retrofit.create( - PlannedPaymentRuleService::class.java - ) - } - val coinbaseService: CoinbaseService by lazy { retrofit.create(CoinbaseService::class.java) } - val githubService: GithubService by lazy { retrofit.create(GithubService::class.java) } - val nukeService: NukeService by lazy { retrofit.create(NukeService::class.java) } -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/AccountDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/AccountDTO.kt deleted file mode 100644 index 6c7de9818a..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/AccountDTO.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.ivy.data.AccountOld -import com.ivy.wallet.io.persistence.data.AccountEntity -import java.util.* - -data class AccountDTO( - val name: String, - val currency: String? = null, - val color: Int = 0, - val icon: String? = null, - val orderNum: Double = 0.0, - val includeInBalance: Boolean = true, - - val id: UUID = UUID.randomUUID() -) { - fun toEntity(): AccountEntity = AccountEntity( - name = name, - currency = currency, - color = color, - icon = icon, - orderNum = orderNum, - includeInBalance = includeInBalance, - id = id, - isSynced = true, - isDeleted = false - ) -} - -fun AccountOld.toDTO(): AccountDTO = AccountDTO( - name = name, - currency = currency, - color = color, - icon = icon, - orderNum = orderNum, - includeInBalance = includeInBalance, - id = id, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/BudgetDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/BudgetDTO.kt deleted file mode 100644 index accd16718f..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/BudgetDTO.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.ivy.data.Budget -import com.ivy.wallet.io.persistence.data.BudgetEntity -import java.util.* - -data class BudgetDTO( - val name: String, - val amount: Double, - - val categoryIdsSerialized: String?, - val accountIdsSerialized: String?, - - val orderId: Double, - val id: UUID = UUID.randomUUID() -) { - fun toEntity(): BudgetEntity = BudgetEntity( - name = name, - amount = amount, - categoryIdsSerialized = categoryIdsSerialized, - accountIdsSerialized = accountIdsSerialized, - orderId = orderId, - id = id, - isSynced = true, - isDeleted = false - ) - - companion object { - fun serialize(ids: List): String { - return ids.joinToString(separator = ",") - } - - fun type(categoriesCount: Int): String { - return when (categoriesCount) { - 0 -> "Total Budget" - 1 -> "Category Budget" - else -> "Multi-Category ($categoriesCount) Budget" - } - } - } - - fun parseCategoryIds(): List { - return parseIdsString(categoryIdsSerialized) - } - - fun parseAccountIds(): List { - return parseIdsString(accountIdsSerialized) - } - - private fun parseIdsString(idsString: String?): List { - return try { - if (idsString == null) return emptyList() - - idsString - .split(",") - .map { UUID.fromString(it) } - } catch (e: Exception) { - e.printStackTrace() - emptyList() - } - } - - - fun validate(): Boolean { - return name.isNotEmpty() && amount > 0.0 - } -} - -fun Budget.toDTO(): BudgetDTO = BudgetDTO( - name = name, - amount = amount, - categoryIdsSerialized = categoryIdsSerialized, - accountIdsSerialized = accountIdsSerialized, - orderId = orderId, - id = id, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/CategoryDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/CategoryDTO.kt deleted file mode 100644 index 9de2033d40..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/CategoryDTO.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.ivy.data.CategoryOld -import com.ivy.wallet.io.persistence.data.CategoryEntity -import java.util.* - -data class CategoryDTO( - val name: String, - val color: Int = 0, - val icon: String? = null, - val orderNum: Double = 0.0, - - val id: UUID = UUID.randomUUID() -) { - fun toEntity(): CategoryEntity = CategoryEntity( - name = name, - color = color, - icon = icon, - orderNum = orderNum, - isSynced = true, - isDeleted = false, - id = id - ) -} - -fun CategoryOld.toDTO(): CategoryDTO = CategoryDTO( - name = name, - color = color, - icon = icon, - orderNum = orderNum, - id = id -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/LoanDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/LoanDTO.kt deleted file mode 100644 index 75d1dd6666..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/LoanDTO.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.ivy.data.loan.Loan -import com.ivy.data.loan.LoanType -import com.ivy.wallet.io.persistence.data.LoanEntity -import java.util.* - -data class LoanDTO( - val name: String, - val amount: Double, - val type: LoanType, - val color: Int = 0, - val icon: String? = null, - val orderNum: Double = 0.0, - val accountId: UUID? = null, - - val id: UUID = UUID.randomUUID() -) { - fun toEntity(): LoanEntity = LoanEntity( - name = name, - amount = amount, - type = type, - color = color, - icon = icon, - orderNum = orderNum, - accountId = accountId, - id = id, - - isSynced = true, - isDeleted = false - ) - - fun humanReadableType(): String { - return if (type == LoanType.BORROW) "BORROWED" else "LENT" - } -} - -fun Loan.toDTO(): LoanDTO = LoanDTO( - name = name, - amount = amount, - type = type, - color = color, - icon = icon, - orderNum = orderNum, - accountId = accountId, - id = id, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/LoanRecordDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/LoanRecordDTO.kt deleted file mode 100644 index fa50734db5..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/LoanRecordDTO.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.ivy.data.loan.LoanRecord -import com.ivy.wallet.io.persistence.data.LoanRecordEntity -import java.time.LocalDateTime -import java.util.* - -data class LoanRecordDTO( - val loanId: UUID, - val amount: Double, - val note: String? = null, - val dateTime: LocalDateTime, - val interest: Boolean = false, - val accountId: UUID? = null, - //This is used store the converted amount for currencies which are different from the loan account currency - val convertedAmount: Double? = null, - - val id: UUID = UUID.randomUUID() -) { - fun toEntity(): LoanRecordEntity = LoanRecordEntity( - loanId = loanId, - amount = amount, - note = note, - dateTime = dateTime, - interest = interest, - accountId = accountId, - convertedAmount = convertedAmount, - id = id, - - isSynced = true, - isDeleted = false - ) -} - -fun LoanRecord.toDTO(): LoanRecordDTO = LoanRecordDTO( - loanId = loanId, - amount = amount, - note = note, - dateTime = dateTime, - interest = interest, - accountId = accountId, - convertedAmount = convertedAmount, - id = id, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/PlannedPaymentRuleDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/PlannedPaymentRuleDTO.kt deleted file mode 100644 index 5afdc746d1..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/PlannedPaymentRuleDTO.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.ivy.data.planned.IntervalType -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.wallet.io.persistence.data.PlannedPaymentRuleEntity -import java.time.LocalDateTime -import java.util.* - -data class PlannedPaymentRuleDTO( - val startDate: LocalDateTime?, - val intervalN: Int?, - val intervalType: IntervalType?, - val oneTime: Boolean, - - val type: TrnTypeOld, - val accountId: UUID, - val amount: Double = 0.0, - val categoryId: UUID? = null, - val title: String? = null, - val description: String? = null, - - val id: UUID = UUID.randomUUID() -) { - fun toEntity(): PlannedPaymentRuleEntity = PlannedPaymentRuleEntity( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime, - type = type, - accountId = accountId, - amount = amount, - categoryId = categoryId, - title = title, - id = id, - - isSynced = true, - isDeleted = false - ) -} - -fun PlannedPaymentRule.toDTO(): PlannedPaymentRuleDTO = PlannedPaymentRuleDTO( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime, - type = type, - accountId = accountId, - amount = amount, - categoryId = categoryId, - title = title, - id = id, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/TransactionDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/TransactionDTO.kt deleted file mode 100644 index 3f813b724e..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/TransactionDTO.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.wallet.io.persistence.data.TransactionEntity -import java.time.LocalDateTime -import java.util.* - -data class TransactionDTO( - val accountId: UUID, - val type: TrnTypeOld, - val amount: Double, - val toAccountId: UUID? = null, - val toAmount: Double? = null, - val title: String? = null, - val description: String? = null, - val dateTime: LocalDateTime? = null, - val categoryId: UUID? = null, - val dueDate: LocalDateTime? = null, - - val recurringRuleId: UUID? = null, - - val attachmentUrl: String? = null, - - //This refers to the loan id that is linked with a transaction - val loanId: UUID? = null, - - //This refers to the loan record id that is linked with a transaction - val loanRecordId: UUID? = null, - - val id: UUID = UUID.randomUUID() -) { - fun toEntity(): TransactionEntity = TransactionEntity( - accountId = accountId, - type = type, - amount = amount, - toAccountId = toAccountId, - toAmount = toAmount, - title = title, - description = description, - dateTime = dateTime, - categoryId = categoryId, - dueDate = dueDate, - recurringRuleId = recurringRuleId, - attachmentUrl = attachmentUrl, - loanId = loanId, - loanRecordId = loanRecordId, - id = id, - - isSynced = true, - isDeleted = false - ) -} - -fun TransactionOld.toDTO(): TransactionDTO = TransactionDTO( - accountId = accountId, - type = type, - amount = amount.toDouble(), - toAccountId = toAccountId, - toAmount = toAmount.toDouble(), - title = title, - description = description, - dateTime = dateTime, - categoryId = categoryId, - dueDate = dueDate, - recurringRuleId = recurringRuleId, - attachmentUrl = attachmentUrl, - loanId = loanId, - loanRecordId = loanRecordId, - id = id, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/data/UserDTO.kt b/temp-network/src/main/java/com/ivy/temp/network/data/UserDTO.kt deleted file mode 100644 index 03473edb1e..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/data/UserDTO.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.ivy.wallet.io.network.data - -import com.google.gson.annotations.SerializedName -import com.ivy.data.user.AuthProviderType -import com.ivy.data.user.User -import com.ivy.wallet.io.persistence.data.UserEntity -import java.util.* - -data class UserDTO( - val email: String, - val authProviderType: AuthProviderType, - var firstName: String, - val lastName: String?, - @SerializedName("profilePictureUrl") - val profilePicture: String?, - val color: Int, - - val testUser: Boolean = false, - var id: UUID -) { - fun toEntity(): UserEntity = UserEntity( - email = email, - authProviderType = authProviderType, - firstName = firstName, - lastName = lastName, - profilePicture = profilePicture, - color = color, - testUser = testUser, - id = id, - ) - - fun names(): String = firstName + if (lastName != null) " $lastName" else "" -} - -fun User.toDTO(): UserDTO = UserDTO( - email = email, - authProviderType = authProviderType, - firstName = firstName, - lastName = lastName, - profilePicture = profilePicture, - color = color, - testUser = testUser, - id = id, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/error/ErrorCode.kt b/temp-network/src/main/java/com/ivy/temp/network/error/ErrorCode.kt deleted file mode 100644 index c7b71a5401..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/error/ErrorCode.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.ivy.wallet.io.network.error - -enum class ErrorCode(val code: Int) { - SERVER_EXCEPTION(666), - PARSE(-2), - INPUT(-1), - - STATE_ERROR(7), - - SECURITY(13), - PERMISSION_ERROR(14), - - CATEGORY_NOT_FOUND(4041), - LABEL_NOT_FOUND(4042), - TASK_NOT_FOUND(4043), - NOTE_NOT_FOUND(4044), - EVENT_NOT_FOUND(4045), - CUSTOM_FIELD_NOT_FOUND(4046), - - NOT_IVY_ATTACHMENT(7404), - - UNKNOWN(-666) -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/error/NetworkError.kt b/temp-network/src/main/java/com/ivy/temp/network/error/NetworkError.kt deleted file mode 100644 index c02e2b6fb0..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/error/NetworkError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.ivy.wallet.io.network.error - -import java.io.IOException - -class NetworkError(val restError: RestError) : - IOException("Network error: ${restError.errorCode.code} - ${restError.msg}") \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/error/RestError.kt b/temp-network/src/main/java/com/ivy/temp/network/error/RestError.kt deleted file mode 100644 index d31e49e6a7..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/error/RestError.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.network.error - -import com.google.gson.annotations.SerializedName - -data class RestError( - @SerializedName("errorCode") - val errorCode: ErrorCode, - @SerializedName("msg") - val msg: String? -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/account/AccountsResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/account/AccountsResponse.kt deleted file mode 100644 index fd0d885d68..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/account/AccountsResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.account - -import com.ivy.wallet.io.network.data.AccountDTO - - -data class AccountsResponse( - val accounts: List -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/account/DeleteAccountRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/account/DeleteAccountRequest.kt deleted file mode 100644 index 54c6592e80..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/account/DeleteAccountRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.account - -import java.util.* - -data class DeleteAccountRequest( - val id: UUID? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/account/UpdateAccountRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/account/UpdateAccountRequest.kt deleted file mode 100644 index d2136e8a6d..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/account/UpdateAccountRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.account - -import com.ivy.wallet.io.network.data.AccountDTO - -data class UpdateAccountRequest( - val account: AccountDTO? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/AuthResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/AuthResponse.kt deleted file mode 100644 index 39e89b27b8..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/AuthResponse.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName -import com.ivy.wallet.io.network.data.UserDTO - -data class AuthResponse( - @SerializedName("user") - val user: UserDTO, - @SerializedName("sessionToken") - val sessionToken: String -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/GoogleSignInRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/GoogleSignInRequest.kt deleted file mode 100644 index 44712bade0..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/GoogleSignInRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName - -data class GoogleSignInRequest( - @SerializedName("googleIdToken") - val googleIdToken: String, - @SerializedName("fcmToken") - val fcmToken: String -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/InitiateResetPasswordRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/InitiateResetPasswordRequest.kt deleted file mode 100644 index 97fb375400..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/InitiateResetPasswordRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName - -data class InitiateResetPasswordRequest( - @SerializedName("email") - val email: String? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/InitiateResetPasswordResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/InitiateResetPasswordResponse.kt deleted file mode 100644 index 71aab08688..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/InitiateResetPasswordResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName - -data class InitiateResetPasswordResponse( - @SerializedName("email") - val email: String -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/ResetPasswordRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/ResetPasswordRequest.kt deleted file mode 100644 index 9d60845a14..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/ResetPasswordRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName - -data class ResetPasswordRequest( - @SerializedName("email") - val email: String? = null, - @SerializedName("newPassword") - val newPassword: String? = null, - @SerializedName("otc") - val otc: String? = null, - @SerializedName("fcmToken") - val fcmToken: String? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/SignInRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/SignInRequest.kt deleted file mode 100644 index ae45c5581f..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/SignInRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName - -data class SignInRequest( - @SerializedName("email") - private val email: String, - @SerializedName("password") - private val password: String, - @SerializedName("fcmToken") - private val fcmToken: String -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/SignUpRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/SignUpRequest.kt deleted file mode 100644 index 50cb819ac4..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/SignUpRequest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName - -data class SignUpRequest( - @SerializedName("email") - val email: String, - @SerializedName("password") - val password: String, - @SerializedName("firstName") - val firstName: String, - @SerializedName("lastName") - val lastName: String? = null, - @SerializedName("color") - val color: Int = 0, - @SerializedName("fcmToken") - val fcmToken: String -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/UpdateUserInfoRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/UpdateUserInfoRequest.kt deleted file mode 100644 index 0598d6007a..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/UpdateUserInfoRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName - -data class UpdateUserInfoRequest( - @SerializedName("firstName") - val firstName: String? = null, - @SerializedName("lastName") - val lastName: String? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/auth/UpdateUserInfoResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/auth/UpdateUserInfoResponse.kt deleted file mode 100644 index 67a9f56b74..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/auth/UpdateUserInfoResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.ivy.wallet.io.network.request.auth - -import com.google.gson.annotations.SerializedName -import com.ivy.data.user.User - -data class UpdateUserInfoResponse( - @SerializedName("user") - val user: User -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/budget/BudgetsResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/budget/BudgetsResponse.kt deleted file mode 100644 index 0a390092f8..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/budget/BudgetsResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.budget - -import com.ivy.wallet.io.network.data.BudgetDTO - - -data class BudgetsResponse( - val budgets: List -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/budget/CrupdateBudgetRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/budget/CrupdateBudgetRequest.kt deleted file mode 100644 index 4273e5efb4..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/budget/CrupdateBudgetRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.budget - -import com.ivy.wallet.io.network.data.BudgetDTO - - -data class CrupdateBudgetRequest( - val budget: BudgetDTO? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/budget/DeleteBudgetRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/budget/DeleteBudgetRequest.kt deleted file mode 100644 index f8ede15993..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/budget/DeleteBudgetRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.budget - -import java.util.* - -data class DeleteBudgetRequest( - val id: UUID? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/category/DeleteWalletCategoryRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/category/DeleteWalletCategoryRequest.kt deleted file mode 100644 index f3ae02e35b..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/category/DeleteWalletCategoryRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.category - -import java.util.* - -data class DeleteWalletCategoryRequest( - val id: UUID? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/category/UpdateWalletCategoryRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/category/UpdateWalletCategoryRequest.kt deleted file mode 100644 index 5642bce39d..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/category/UpdateWalletCategoryRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.category - -import com.ivy.wallet.io.network.data.CategoryDTO - -data class UpdateWalletCategoryRequest( - val category: CategoryDTO? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/category/WalletCategoriesResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/category/WalletCategoriesResponse.kt deleted file mode 100644 index 41ff386004..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/category/WalletCategoriesResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.category - -import com.ivy.wallet.io.network.data.CategoryDTO - - -data class WalletCategoriesResponse( - val categories: List -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/currency/CoinbaseRatesResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/currency/CoinbaseRatesResponse.kt deleted file mode 100644 index bced987799..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/currency/CoinbaseRatesResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.network.request.currency - -data class CoinbaseRatesResponse( - val data: ExchangeRatesData -) - -data class ExchangeRatesData( - val currency: String, - val rates: Map -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/github/OpenIssueRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/github/OpenIssueRequest.kt deleted file mode 100644 index e7a60acd38..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/github/OpenIssueRequest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.ivy.wallet.io.network.request.github - -data class OpenIssueRequest( - val title: String, - val body: String, -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/github/OpenIssueResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/github/OpenIssueResponse.kt deleted file mode 100644 index d20b7d628e..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/github/OpenIssueResponse.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ivy.wallet.io.network.request.github - -data class OpenIssueResponse( - val url: String -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/loan/DeleteLoanRecordRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/loan/DeleteLoanRecordRequest.kt deleted file mode 100644 index d48401c41f..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/loan/DeleteLoanRecordRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.loan - -import java.util.* - -data class DeleteLoanRecordRequest( - val id: UUID? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/loan/DeleteLoanRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/loan/DeleteLoanRequest.kt deleted file mode 100644 index 307e48613e..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/loan/DeleteLoanRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.loan - -import java.util.* - -data class DeleteLoanRequest( - val id: UUID? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/loan/LoanRecordsResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/loan/LoanRecordsResponse.kt deleted file mode 100644 index be6a3e030a..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/loan/LoanRecordsResponse.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.loan - -import com.ivy.wallet.io.network.data.LoanRecordDTO - -data class LoanRecordsResponse( - val loanRecords: List -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/loan/LoansResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/loan/LoansResponse.kt deleted file mode 100644 index 6397632fa8..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/loan/LoansResponse.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.loan - -import com.ivy.wallet.io.network.data.LoanDTO - -data class LoansResponse( - val loans: List -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/loan/UpdateLoanRecordRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/loan/UpdateLoanRecordRequest.kt deleted file mode 100644 index b35a489089..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/loan/UpdateLoanRecordRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.loan - -import com.ivy.wallet.io.network.data.LoanRecordDTO - -data class UpdateLoanRecordRequest( - val loanRecord: LoanRecordDTO? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/loan/UpdateLoanRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/loan/UpdateLoanRequest.kt deleted file mode 100644 index 7b3f48408a..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/loan/UpdateLoanRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.loan - -import com.ivy.wallet.io.network.data.LoanDTO - -data class UpdateLoanRequest( - val loan: LoanDTO? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/planned/DeletePlannedPaymentRuleRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/planned/DeletePlannedPaymentRuleRequest.kt deleted file mode 100644 index 3974efafbe..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/planned/DeletePlannedPaymentRuleRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.planned - -import java.util.* - -data class DeletePlannedPaymentRuleRequest( - val id: UUID? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/planned/PlannedPaymentRulesResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/planned/PlannedPaymentRulesResponse.kt deleted file mode 100644 index 5009e0cbaa..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/planned/PlannedPaymentRulesResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.planned - -import com.ivy.wallet.io.network.data.PlannedPaymentRuleDTO - - -data class PlannedPaymentRulesResponse( - val rules: List -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/planned/UpdatePlannedPaymentRuleRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/planned/UpdatePlannedPaymentRuleRequest.kt deleted file mode 100644 index f0e53df538..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/planned/UpdatePlannedPaymentRuleRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.planned - -import com.ivy.wallet.io.network.data.PlannedPaymentRuleDTO - -data class UpdatePlannedPaymentRuleRequest( - val rule: PlannedPaymentRuleDTO? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/transaction/DeleteTransactionRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/transaction/DeleteTransactionRequest.kt deleted file mode 100644 index 688e9f559c..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/transaction/DeleteTransactionRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.transaction - -import java.util.* - -data class DeleteTransactionRequest( - val id: UUID? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/transaction/TransactionsResponse.kt b/temp-network/src/main/java/com/ivy/temp/network/request/transaction/TransactionsResponse.kt deleted file mode 100644 index e09e65b88f..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/transaction/TransactionsResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.request.transaction - -import com.ivy.wallet.io.network.data.TransactionDTO - - -data class TransactionsResponse( - val transactions: List -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/request/transaction/UpdateTransactionRequest.kt b/temp-network/src/main/java/com/ivy/temp/network/request/transaction/UpdateTransactionRequest.kt deleted file mode 100644 index d0c26b57a0..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/request/transaction/UpdateTransactionRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.wallet.io.network.request.transaction - -import com.ivy.wallet.io.network.data.TransactionDTO - -data class UpdateTransactionRequest( - val transaction: TransactionDTO? = null -) \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/AccountService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/AccountService.kt deleted file mode 100644 index 7e16aab6d2..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/AccountService.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.account.AccountsResponse -import com.ivy.wallet.io.network.request.account.DeleteAccountRequest -import com.ivy.wallet.io.network.request.account.UpdateAccountRequest -import retrofit2.http.* - -interface AccountService { - @POST("/wallet/accounts/update") - suspend fun update(@Body request: UpdateAccountRequest) - - @GET("/wallet/accounts") - suspend fun get(@Query("after") after: Long? = null): AccountsResponse - - @HTTP(method = "DELETE", path = "/wallet/accounts/delete", hasBody = true) - suspend fun delete(@Body request: DeleteAccountRequest) -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/AuthService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/AuthService.kt deleted file mode 100644 index 456cf2fb6d..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/AuthService.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.auth.* -import retrofit2.http.Body -import retrofit2.http.POST - -interface AuthService { - @POST("/auth/google-sign-in") - suspend fun googleSignIn(@Body request: GoogleSignInRequest): AuthResponse - - @POST("/auth/initiate-reset-password") - suspend fun initiateResetPassword( - @Body request: InitiateResetPasswordRequest - ): InitiateResetPasswordResponse - - @POST("/auth/reset-password") - suspend fun resetPassword(@Body request: ResetPasswordRequest): AuthResponse - - @POST("/auth/update-user-info") - suspend fun updateUserInfo(@Body request: UpdateUserInfoRequest): UpdateUserInfoResponse - -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/BudgetService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/BudgetService.kt deleted file mode 100644 index 4097ced02f..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/BudgetService.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.budget.BudgetsResponse -import com.ivy.wallet.io.network.request.budget.CrupdateBudgetRequest -import com.ivy.wallet.io.network.request.budget.DeleteBudgetRequest -import retrofit2.http.* - -interface BudgetService { - @POST("/wallet/budgets/update") - suspend fun update(@Body request: CrupdateBudgetRequest) - - @GET("/wallet/budgets") - suspend fun get(@Query("after") after: Long? = null): BudgetsResponse - - @HTTP(method = "DELETE", path = "/wallet/budgets/delete", hasBody = true) - suspend fun delete(@Body request: DeleteBudgetRequest) -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/CategoryService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/CategoryService.kt deleted file mode 100644 index 06f9778ead..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/CategoryService.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.category.DeleteWalletCategoryRequest -import com.ivy.wallet.io.network.request.category.UpdateWalletCategoryRequest -import com.ivy.wallet.io.network.request.category.WalletCategoriesResponse -import retrofit2.http.* - -interface CategoryService { - @POST("/wallet/categories/update") - suspend fun update(@Body request: UpdateWalletCategoryRequest) - - @GET("/wallet/categories") - suspend fun get(@Query("after") after: Long? = null): WalletCategoriesResponse - - @HTTP(method = "DELETE", path = "/wallet/categories/delete", hasBody = true) - suspend fun delete(@Body request: DeleteWalletCategoryRequest) -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/CoinbaseService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/CoinbaseService.kt deleted file mode 100644 index 558af903b7..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/CoinbaseService.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.currency.CoinbaseRatesResponse -import retrofit2.http.GET -import retrofit2.http.Url - -interface CoinbaseService { - companion object { - fun exchangeRatesUrl( - baseCurrencyCode: String - ): String { - return "https://api.coinbase.com/v2/exchange-rates?currency=${baseCurrencyCode}" - } - } - - @GET - suspend fun getExchangeRates( - @Url url: String, - ): CoinbaseRatesResponse -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/ExpImagesService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/ExpImagesService.kt deleted file mode 100644 index 4a9af2031c..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/ExpImagesService.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.service - -import retrofit2.http.GET - -interface ExpImagesService { - @GET - suspend fun fetchImages(): List -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/GithubService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/GithubService.kt deleted file mode 100644 index edc5446c84..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/GithubService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.github.OpenIssueRequest -import com.ivy.wallet.io.network.request.github.OpenIssueResponse -import retrofit2.http.Body -import retrofit2.http.Header -import retrofit2.http.POST - -interface GithubService { - companion object { - const val BASE_URL = "https://api.github.com" - const val OPEN_ISSUE_URL = "$BASE_URL/repos/Ivy-Apps/ivy-wallet/issues" - - const val GITHUB_SERVICE_ACC_USERNAME = "ivywallet" - - //Split Github Access token in two parts so Github doesn't delete it - //because "Personal access token was found in commit." - const val GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_1 = "ghp_MuvrbtIH897" - const val GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_2 = "JASL6i8mBvXJ3aM7DLk4U9Gwq" - } - - @POST(OPEN_ISSUE_URL) - suspend fun openIssue( - @Header("Accept") accept: String = "application/vnd.github.v3+json", - @Body request: OpenIssueRequest - ): OpenIssueResponse -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/LoanService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/LoanService.kt deleted file mode 100644 index 814a73b21f..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/LoanService.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.loan.* -import retrofit2.http.* - -interface LoanService { - @POST("/wallet/loans/update") - suspend fun update(@Body request: UpdateLoanRequest) - - @GET("/wallet/loans") - suspend fun get(@Query("after") after: Long? = null): LoansResponse - - @HTTP(method = "DELETE", path = "/wallet/loans/delete", hasBody = true) - suspend fun delete(@Body request: DeleteLoanRequest) - - //LOAN RECORDS ---------------------------------------------------------------- - @POST("/wallet/loans/update-record") - suspend fun updateRecord(@Body request: UpdateLoanRecordRequest) - - @GET("/wallet/loans/records") - suspend fun getRecords(@Query("after") after: Long? = null): LoanRecordsResponse - - @HTTP(method = "DELETE", path = "/wallet/loans/delete-record", hasBody = true) - suspend fun deleteRecord(@Body request: DeleteLoanRecordRequest) -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/NukeService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/NukeService.kt deleted file mode 100644 index 064ee889ad..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/NukeService.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ivy.wallet.io.network.service - -import retrofit2.http.POST - -interface NukeService { - @POST("wallet/nuke/delete-all-user-data") - suspend fun deleteAllUserData() -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/PlannedPaymentRuleService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/PlannedPaymentRuleService.kt deleted file mode 100644 index c5d2d41759..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/PlannedPaymentRuleService.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.planned.DeletePlannedPaymentRuleRequest -import com.ivy.wallet.io.network.request.planned.PlannedPaymentRulesResponse -import com.ivy.wallet.io.network.request.planned.UpdatePlannedPaymentRuleRequest -import retrofit2.http.* - -interface PlannedPaymentRuleService { - @POST("/wallet/planned-payments/update") - suspend fun update(@Body request: UpdatePlannedPaymentRuleRequest) - - @GET("/wallet/planned-payments") - suspend fun get(@Query("after") after: Long? = null): PlannedPaymentRulesResponse - - @HTTP(method = "DELETE", path = "/wallet/planned-payments/delete", hasBody = true) - suspend fun delete(@Body request: DeletePlannedPaymentRuleRequest) -} \ No newline at end of file diff --git a/temp-network/src/main/java/com/ivy/temp/network/service/TransactionService.kt b/temp-network/src/main/java/com/ivy/temp/network/service/TransactionService.kt deleted file mode 100644 index 959fcd9071..0000000000 --- a/temp-network/src/main/java/com/ivy/temp/network/service/TransactionService.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.ivy.wallet.io.network.service - -import com.ivy.wallet.io.network.request.transaction.DeleteTransactionRequest -import com.ivy.wallet.io.network.request.transaction.TransactionsResponse -import com.ivy.wallet.io.network.request.transaction.UpdateTransactionRequest -import retrofit2.http.* - -interface TransactionService { - @POST("/wallet/transactions/update") - suspend fun update(@Body request: UpdateTransactionRequest) - - @GET("/wallet/transactions") - suspend fun get(@Query("after") after: Long? = null): TransactionsResponse - - @GET("/wallet/transactions/paginated") - suspend fun getPaginated( - @Query("page") page: Int, - @Query("size") size: Int - ): TransactionsResponse - - - @HTTP(method = "DELETE", path = "/wallet/transactions/delete", hasBody = true) - suspend fun delete(@Body request: DeleteTransactionRequest) -} \ No newline at end of file diff --git a/temp-persistence/.gitignore b/temp-persistence/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/temp-persistence/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/temp-persistence/README.md b/temp-persistence/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/temp-persistence/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/120.json b/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/120.json deleted file mode 100644 index 84070e16b9..0000000000 --- a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/120.json +++ /dev/null @@ -1,793 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 120, - "identityHash": "751c82ed72a54493f42000cd47a99137", - "entities": [ - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `currency` TEXT, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `includeInBalance` INTEGER NOT NULL, `seAccountId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "includeInBalance", - "columnName": "includeInBalance", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "seAccountId", - "columnName": "seAccountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `type` TEXT NOT NULL, `amount` REAL NOT NULL, `toAccountId` TEXT, `toAmount` REAL, `title` TEXT, `description` TEXT, `dateTime` INTEGER, `categoryId` TEXT, `dueDate` INTEGER, `recurringRuleId` TEXT, `attachmentUrl` TEXT, `loanId` TEXT, `loanRecordId` TEXT, `seTransactionId` TEXT, `seAutoCategoryId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "toAccountId", - "columnName": "toAccountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "toAmount", - "columnName": "toAmount", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dateTime", - "columnName": "dateTime", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "categoryId", - "columnName": "categoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dueDate", - "columnName": "dueDate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "recurringRuleId", - "columnName": "recurringRuleId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "attachmentUrl", - "columnName": "attachmentUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "loanId", - "columnName": "loanId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "loanRecordId", - "columnName": "loanRecordId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "seTransactionId", - "columnName": "seTransactionId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "seAutoCategoryId", - "columnName": "seAutoCategoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `seCategoryName` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "seCategoryName", - "columnName": "seCategoryName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "wishlist_items", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `price` REAL NOT NULL, `accountId` TEXT NOT NULL, `categoryId` TEXT, `description` TEXT, `plannedDateTime` INTEGER, `orderNum` REAL NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "price", - "columnName": "price", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "categoryId", - "columnName": "categoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "plannedDateTime", - "columnName": "plannedDateTime", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "settings", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`theme` TEXT NOT NULL, `currency` TEXT NOT NULL, `bufferAmount` REAL NOT NULL, `name` TEXT NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "theme", - "columnName": "theme", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "bufferAmount", - "columnName": "bufferAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "planned_payment_rules", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`startDate` INTEGER, `intervalN` INTEGER, `intervalType` TEXT, `oneTime` INTEGER NOT NULL, `type` TEXT NOT NULL, `accountId` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` TEXT, `title` TEXT, `description` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "startDate", - "columnName": "startDate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "intervalN", - "columnName": "intervalN", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "intervalType", - "columnName": "intervalType", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "oneTime", - "columnName": "oneTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "categoryId", - "columnName": "categoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `authProviderType` TEXT NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT, `profilePicture` TEXT, `color` INTEGER NOT NULL, `testUser` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authProviderType", - "columnName": "authProviderType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "firstName", - "columnName": "firstName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastName", - "columnName": "lastName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "profilePicture", - "columnName": "profilePicture", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "testUser", - "columnName": "testUser", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchange_rates", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseCurrency` TEXT NOT NULL, `currency` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`baseCurrency`, `currency`))", - "fields": [ - { - "fieldPath": "baseCurrency", - "columnName": "baseCurrency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "baseCurrency", - "currency" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "budgets", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryIdsSerialized` TEXT, `accountIdsSerialized` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `orderId` REAL NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "categoryIdsSerialized", - "columnName": "categoryIdsSerialized", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "accountIdsSerialized", - "columnName": "accountIdsSerialized", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "orderId", - "columnName": "orderId", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "loans", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `type` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `accountId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "loan_records", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "loanId", - "columnName": "loanId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dateTime", - "columnName": "dateTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "interest", - "columnName": "interest", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "convertedAmount", - "columnName": "convertedAmount", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '751c82ed72a54493f42000cd47a99137')" - ] - } -} \ No newline at end of file diff --git a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/121.json b/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/121.json deleted file mode 100644 index 83dd1a6fb9..0000000000 --- a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/121.json +++ /dev/null @@ -1,707 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 121, - "identityHash": "319b13332051b3e936a5902b2b7a2ad5", - "entities": [ - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `currency` TEXT, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `includeInBalance` INTEGER NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "includeInBalance", - "columnName": "includeInBalance", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `type` TEXT NOT NULL, `amount` REAL NOT NULL, `toAccountId` TEXT, `toAmount` REAL, `title` TEXT, `description` TEXT, `dateTime` INTEGER, `categoryId` TEXT, `dueDate` INTEGER, `recurringRuleId` TEXT, `attachmentUrl` TEXT, `loanId` TEXT, `loanRecordId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "toAccountId", - "columnName": "toAccountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "toAmount", - "columnName": "toAmount", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dateTime", - "columnName": "dateTime", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "categoryId", - "columnName": "categoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dueDate", - "columnName": "dueDate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "recurringRuleId", - "columnName": "recurringRuleId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "attachmentUrl", - "columnName": "attachmentUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "loanId", - "columnName": "loanId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "loanRecordId", - "columnName": "loanRecordId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "settings", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`theme` TEXT NOT NULL, `currency` TEXT NOT NULL, `bufferAmount` REAL NOT NULL, `name` TEXT NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "theme", - "columnName": "theme", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "bufferAmount", - "columnName": "bufferAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "planned_payment_rules", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`startDate` INTEGER, `intervalN` INTEGER, `intervalType` TEXT, `oneTime` INTEGER NOT NULL, `type` TEXT NOT NULL, `accountId` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` TEXT, `title` TEXT, `description` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "startDate", - "columnName": "startDate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "intervalN", - "columnName": "intervalN", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "intervalType", - "columnName": "intervalType", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "oneTime", - "columnName": "oneTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "categoryId", - "columnName": "categoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `authProviderType` TEXT NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT, `profilePicture` TEXT, `color` INTEGER NOT NULL, `testUser` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authProviderType", - "columnName": "authProviderType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "firstName", - "columnName": "firstName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastName", - "columnName": "lastName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "profilePicture", - "columnName": "profilePicture", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "testUser", - "columnName": "testUser", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchange_rates", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseCurrency` TEXT NOT NULL, `currency` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`baseCurrency`, `currency`))", - "fields": [ - { - "fieldPath": "baseCurrency", - "columnName": "baseCurrency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "baseCurrency", - "currency" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "budgets", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryIdsSerialized` TEXT, `accountIdsSerialized` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `orderId` REAL NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "categoryIdsSerialized", - "columnName": "categoryIdsSerialized", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "accountIdsSerialized", - "columnName": "accountIdsSerialized", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "orderId", - "columnName": "orderId", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "loans", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `type` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `accountId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "loan_records", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "loanId", - "columnName": "loanId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dateTime", - "columnName": "dateTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "interest", - "columnName": "interest", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "convertedAmount", - "columnName": "convertedAmount", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '319b13332051b3e936a5902b2b7a2ad5')" - ] - } -} \ No newline at end of file diff --git a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/122.json b/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/122.json deleted file mode 100644 index 295aa84e59..0000000000 --- a/temp-persistence/schemas/com.ivy.wallet.io.persistence.IvyRoomDatabase/122.json +++ /dev/null @@ -1,707 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 122, - "identityHash": "319b13332051b3e936a5902b2b7a2ad5", - "entities": [ - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `currency` TEXT, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `includeInBalance` INTEGER NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "includeInBalance", - "columnName": "includeInBalance", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `type` TEXT NOT NULL, `amount` REAL NOT NULL, `toAccountId` TEXT, `toAmount` REAL, `title` TEXT, `description` TEXT, `dateTime` INTEGER, `categoryId` TEXT, `dueDate` INTEGER, `recurringRuleId` TEXT, `attachmentUrl` TEXT, `loanId` TEXT, `loanRecordId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "toAccountId", - "columnName": "toAccountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "toAmount", - "columnName": "toAmount", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dateTime", - "columnName": "dateTime", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "categoryId", - "columnName": "categoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dueDate", - "columnName": "dueDate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "recurringRuleId", - "columnName": "recurringRuleId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "attachmentUrl", - "columnName": "attachmentUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "loanId", - "columnName": "loanId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "loanRecordId", - "columnName": "loanRecordId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "settings", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`theme` TEXT NOT NULL, `currency` TEXT NOT NULL, `bufferAmount` REAL NOT NULL, `name` TEXT NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "theme", - "columnName": "theme", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "bufferAmount", - "columnName": "bufferAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "planned_payment_rules", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`startDate` INTEGER, `intervalN` INTEGER, `intervalType` TEXT, `oneTime` INTEGER NOT NULL, `type` TEXT NOT NULL, `accountId` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` TEXT, `title` TEXT, `description` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "startDate", - "columnName": "startDate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "intervalN", - "columnName": "intervalN", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "intervalType", - "columnName": "intervalType", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "oneTime", - "columnName": "oneTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "categoryId", - "columnName": "categoryId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `authProviderType` TEXT NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT, `profilePicture` TEXT, `color` INTEGER NOT NULL, `testUser` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authProviderType", - "columnName": "authProviderType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "firstName", - "columnName": "firstName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastName", - "columnName": "lastName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "profilePicture", - "columnName": "profilePicture", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "testUser", - "columnName": "testUser", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchange_rates", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseCurrency` TEXT NOT NULL, `currency` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`baseCurrency`, `currency`))", - "fields": [ - { - "fieldPath": "baseCurrency", - "columnName": "baseCurrency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "baseCurrency", - "currency" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "budgets", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryIdsSerialized` TEXT, `accountIdsSerialized` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `orderId` REAL NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "categoryIdsSerialized", - "columnName": "categoryIdsSerialized", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "accountIdsSerialized", - "columnName": "accountIdsSerialized", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "orderId", - "columnName": "orderId", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "loans", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `type` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `accountId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "color", - "columnName": "color", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "orderNum", - "columnName": "orderNum", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "loan_records", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "loanId", - "columnName": "loanId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "dateTime", - "columnName": "dateTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "interest", - "columnName": "interest", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "accountId", - "columnName": "accountId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "convertedAmount", - "columnName": "convertedAmount", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "isSynced", - "columnName": "isSynced", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isDeleted", - "columnName": "isDeleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '319b13332051b3e936a5902b2b7a2ad5')" - ] - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/AndroidManifest.xml b/temp-persistence/src/main/AndroidManifest.xml deleted file mode 100644 index dfd446a4f1..0000000000 --- a/temp-persistence/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRate.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRate.kt deleted file mode 100644 index ae170b324e..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRate.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.ivy.temp.persistence - -@Deprecated("old") -data class ExchangeRate( - val baseCurrency: String, - val currency: String, - val rate: Double, -) { - fun toEntity(): ExchangeRateEntity = ExchangeRateEntity( - baseCurrency = baseCurrency, - currency = currency, - rate = rate - ) -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRateDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRateDao.kt deleted file mode 100644 index 18e7e4dfe7..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRateDao.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.ivy.temp.persistence - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import kotlinx.coroutines.flow.Flow - -@Deprecated("old") -@Dao -interface ExchangeRateDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: ExchangeRateEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(values: List) - - @Query("SELECT * FROM exchange_rates") - suspend fun findAllSuspend(): List - - @Query("SELECT * FROM exchange_rates") - fun findAll(): Flow> - - @Query("SELECT * FROM exchange_rates WHERE baseCurrency = :baseCurrency AND currency = :currency") - suspend fun findByBaseCurrencyAndCurrency( - baseCurrency: String, - currency: String - ): ExchangeRateEntity? - - @Query("DELETE FROM exchange_rates") - suspend fun deleteALl() -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRateEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRateEntity.kt deleted file mode 100644 index e5f9fa88f9..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/ExchangeRateEntity.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ivy.temp.persistence - -import androidx.room.Entity - -@Deprecated("old") -@Entity(tableName = "exchange_rates", primaryKeys = ["baseCurrency", "currency"]) -data class ExchangeRateEntity( - val baseCurrency: String, - val currency: String, - val rate: Double, -) { - fun toDomain(): ExchangeRate = ExchangeRate( - baseCurrency = baseCurrency, - currency = currency, - rate = rate - ) -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/IOEffect.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/IOEffect.kt deleted file mode 100644 index 22677089a5..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/IOEffect.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ivy.temp.persistence - -@Deprecated("will be deleted") -sealed class IOEffect(val item: T) { - class Save(item: T) : IOEffect(item) - class Delete(item: T) : IOEffect(item) -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/IvyRoomDatabase.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/IvyRoomDatabase.kt deleted file mode 100644 index e2fcd14a28..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/IvyRoomDatabase.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.ivy.wallet.io.persistence - -import android.content.Context -import androidx.room.* -import androidx.room.migration.AutoMigrationSpec -import com.ivy.temp.persistence.ExchangeRateDao -import com.ivy.temp.persistence.ExchangeRateEntity -import com.ivy.wallet.io.persistence.dao.* -import com.ivy.wallet.io.persistence.data.* -import com.ivy.wallet.io.persistence.migration.* - - -@Deprecated("don't use! it'll be deleted after data migration") -@Database( - entities = [ - AccountEntity::class, TransactionEntity::class, CategoryEntity::class, - SettingsEntity::class, PlannedPaymentRuleEntity::class, - UserEntity::class, ExchangeRateEntity::class, BudgetEntity::class, - LoanEntity::class, LoanRecordEntity::class - ], - autoMigrations = [ - AutoMigration( - from = 121, - to = 122, - spec = IvyRoomDatabase.DeleteSEMigration::class - ) - ], - version = 123, - exportSchema = true -) -@TypeConverters(RoomTypeConverters::class) -abstract class IvyRoomDatabase : RoomDatabase() { - abstract fun accountDao(): AccountDao - - abstract fun transactionDao(): TransactionDao - - abstract fun categoryDao(): CategoryDao - - abstract fun budgetDao(): BudgetDao - - abstract fun plannedPaymentRuleDao(): PlannedPaymentRuleDao - - abstract fun settingsDao(): SettingsDao - - abstract fun userDao(): UserDao - - abstract fun exchangeRatesDao(): ExchangeRateDao - - abstract fun loanDao(): LoanDao - - abstract fun loanRecordDao(): LoanRecordDao - - companion object { - private const val DB_NAME = "ivywallet.db" - - fun create(applicationContext: Context): IvyRoomDatabase { - return Room - .databaseBuilder( - applicationContext, - IvyRoomDatabase::class.java, DB_NAME - ) - .addMigrations( - Migration105to106_TrnRecurringRules(), - Migration106to107_Wishlist(), - Migration107to108_Sync(), - Migration108to109_Users(), - Migration109to110_PlannedPayments(), - Migration110to111_PlannedPaymentRule(), - Migration111to112_User_testUser(), - Migration112to113_ExchangeRates(), - Migration113to114_Multi_Currency(), - Migration114to115_Category_Account_Icons(), - Migration115to116_Account_Include_In_Balance(), - Migration116to117_SalteEdgeIntgration(), - Migration117to118_Budgets(), - Migration118to119_Loans(), - Migration119to120_LoanTransactions(), - Migration120to121_DropWishlistItem(), - Migration122to123_SubCategories() - ) - .build() - } - } - - suspend fun reset() { - accountDao().deleteAll() - transactionDao().deleteAll() - categoryDao().deleteAll() - settingsDao().deleteAll() - plannedPaymentRuleDao().deleteAll() - userDao().deleteAll() - budgetDao().deleteAll() - loanDao().deleteAll() - loanRecordDao().deleteAll() - } - - @DeleteColumn(tableName = "accounts", columnName = "seAccountId") - @DeleteColumn(tableName = "transactions", columnName = "seTransactionId") - @DeleteColumn(tableName = "transactions", columnName = "seAutoCategoryId") - @DeleteColumn(tableName = "categories", columnName = "seCategoryName") - class DeleteSEMigration : AutoMigrationSpec -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/RoomTypeConverters.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/RoomTypeConverters.kt deleted file mode 100644 index 4c5930027e..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/RoomTypeConverters.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.ivy.wallet.io.persistence - -import androidx.room.TypeConverter -import com.ivy.common.time.deviceTimeProvider -import com.ivy.common.time.epochMilliToDateTime -import com.ivy.common.time.toEpochMilli -import com.ivy.data.Theme -import com.ivy.data.loan.LoanType -import com.ivy.data.planned.IntervalType -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.data.user.AuthProviderType -import java.time.LocalDateTime -import java.util.* - -@Deprecated("old") -@SuppressWarnings("unused") -class RoomTypeConverters { - @TypeConverter - fun saveDate(localDateTime: LocalDateTime?): Long? = localDateTime?.toEpochMilli( - deviceTimeProvider() - ) - - @TypeConverter - fun parseDate(timestampMillis: Long?): LocalDateTime? = timestampMillis?.epochMilliToDateTime() - - @TypeConverter - fun saveUUID(id: UUID?) = id?.toString() - - @TypeConverter - fun parseUUID(id: String?) = id?.let { UUID.fromString(id) } - - @TypeConverter - fun saveTheme(value: Theme?) = value?.name - - @TypeConverter - fun parseTheme(value: String?) = value?.let { Theme.valueOf(it) } - - @TypeConverter - fun saveTransactionType(value: TrnTypeOld?) = value?.name - - @TypeConverter - fun parseTransactionType(value: String?) = value?.let { TrnTypeOld.valueOf(it) } - - @TypeConverter - fun saveAuthProviderType(authProviderType: AuthProviderType?) = authProviderType?.name - - @TypeConverter - fun parseAuthProviderType(value: String?) = value?.let { AuthProviderType.valueOf(it) } - - @TypeConverter - fun saveRecurringIntervalType(value: IntervalType?) = value?.name - - @TypeConverter - fun parseRecurringIntervalType(value: String?) = - value?.let { IntervalType.valueOf(it) } - - @TypeConverter - fun saveLoanType(value: LoanType?) = value?.name - - @TypeConverter - fun parseLoanType(value: String?) = value?.let { LoanType.valueOf(it) } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/SharedPrefs.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/SharedPrefs.kt deleted file mode 100644 index 0aa7d086e9..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/SharedPrefs.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.ivy.wallet.io.persistence - -import android.content.Context -import com.google.gson.Gson - -/** - * Created by iliyan on 13.03.18. - */ -@Deprecated( - message = "migrating to flows", - replaceWith = ReplaceWith("IvyDataStore") -) -class SharedPrefs(appContext: Context) { - companion object { - private const val PREFS_FILENAME = "ivy_wallet_prefs" - - const val ONBOARDING_COMPLETED = "onboarding_completed" - - const val FCM_TOKEN = "fcm_token" - - //Sync - const val LAST_SYNC_DATE_CATEGORIES = "last_sync_date_categories" - const val LAST_SYNC_DATE_BUDGETS = "last_sync_date_budgets" - const val LAST_SYNC_DATE_LOANS = "last_sync_date_loans" - const val LAST_SYNC_DATE_LOAN_RECORDS = "last_sync_date_loan_records" - const val LAST_SYNC_DATE_ACCOUNTS = "last_sync_date_accounts" - const val LAST_SYNC_DATE_TRANSACTIONS = "last_sync_date_transactions" - const val LAST_SYNC_DATE_PLANNED_PAYMENTS = "last_sync_date_planned_payments" - - //----------------------------------------------------------------------------- - - //Analytics - const val ANALYTICS_SESSION_ID = "analytics_session_id" - //------------------------------------------------------------------------------------------ - - //-------------------------------------- UX ------------------------------------------------ - const val LAST_SELECTED_ACCOUNT_ID = "last_selected_account_id" - //-------------------------------------- UX ------------------------------------------------ - - const val SESSION_USER_ID = "session_user_id" - const val SESSION_AUTH_TOKEN = "session_auth_token" - - - //-------------------------------- Bank Integrations temp ---------------------------------- - const val ENABLE_BANK_SYNC = "enable_bank_sync" - //-------------------------------- Bank Integrations temp ---------------------------------- - - //----------------------------- App Settings ----------------------------------------------- - const val APP_LOCK_ENABLED = "lock_app" - const val START_DATE_OF_MONTH = "start_date_of_month" - const val SHOW_NOTIFICATIONS = "show_notifications" - const val HIDE_CURRENT_BALANCE = "hide_current_balance" - const val TRANSFERS_AS_INCOME_EXPENSE = "transfers_as_inc_exp" - //----------------------------- App Settings ----------------------------------------------- - - //-------------------------------- Customer Journey ---------------------------------------- - const val _CARD_DISMISSED = "_cj_dismissed" - //-------------------------------- Customer Journey ---------------------------------------- - - //----------------------------- Others ----------------------------------------------- - const val CATEGORY_SORT_ORDER = "categorySortOrder" - } - - private val preferences = appContext.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE) - private val gson = Gson() - - fun has(key: String): Boolean { - return preferences.contains(key) - } - - val all: Map - get() = preferences.all - - fun putInt(key: String, value: Int) { - val editor = preferences.edit() - editor.putInt(key, value) - editor.apply() - } - - fun putFloat(key: String, value: Float) { - val editor = preferences.edit() - editor.putFloat(key, value) - editor.apply() - } - - fun putDouble(key: String, value: Double) { - val editor = preferences.edit() - editor.putFloat(key, value.toFloat()) - editor.apply() - } - - fun putLong(key: String, value: Long) { - val editor = preferences.edit() - editor.putLong(key, value) - editor.apply() - } - - fun putString(key: String, value: String?) { - val editor = preferences.edit() - editor.putString(key, value) - editor.apply() - } - - fun putBoolean(key: String, value: Boolean) { - val editor = preferences.edit() - editor.putBoolean(key, value) - editor.apply() - } - - fun put(key: String, value: T?) { - val editor = preferences.edit() - editor.putString(key, gson.toJson(value)) - editor.apply() - } - - fun getLong(key: String, defValue: Long): Long { - return preferences.getLong(key, defValue) - } - - fun getInt(key: String, defValue: Int): Int { - return preferences.getInt(key, defValue) - } - - fun getFloat(key: String, defValue: Float): Float { - return preferences.getFloat(key, defValue) - } - - fun getBoolean(key: String, defValue: Boolean): Boolean { - return preferences.getBoolean(key, defValue) - } - - fun getString(key: String): String { - return preferences.getString(key, null) - ?: throw IllegalStateException("SharePrefs key '$key' cannot be null") - } - - fun getString(key: String, defValue: String?): String? { - return preferences.getString(key, defValue) - } - - fun getEpochSeconds(key: String): Long? { - val epochSeconds = preferences.getLong(key, -1) - return if (epochSeconds > 0) epochSeconds else null - } - - operator fun get(key: String, aClass: Class): T? { - val jsonString = preferences.getString(key, null) - return gson.fromJson(jsonString, aClass) - } - - fun remove(key: String) { - preferences.edit().remove(key).apply() - } - - fun removeAll() { - preferences.edit().clear().apply() - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/AccountDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/AccountDao.kt deleted file mode 100644 index 5d2ba713db..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/AccountDao.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.AccountEntity -import kotlinx.coroutines.flow.Flow -import java.util.* - -@Deprecated("use `:core:persistence`") -@Dao -interface AccountDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: AccountEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("SELECT * FROM accounts WHERE isDeleted = 0 ORDER BY orderNum ASC") - suspend fun findAllSuspend(): List - - @Query("SELECT * FROM accounts WHERE isDeleted = 0 ORDER BY orderNum ASC") - fun findAll(): Flow> - - @Query("SELECT * FROM accounts WHERE isSynced = :synced AND isDeleted = :deleted") - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - @Query("SELECT * FROM accounts WHERE id = :id") - suspend fun findById(id: UUID): AccountEntity? - - @Query("UPDATE accounts SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query("DELETE FROM accounts WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("DELETE FROM accounts") - suspend fun deleteAll() - - @Query("SELECT MIN(orderNum) FROM accounts") - suspend fun findMinOrderNum(): Double - - @Query("SELECT MAX(orderNum) FROM accounts") - suspend fun findMaxOrderNum(): Double? -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/BudgetDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/BudgetDao.kt deleted file mode 100644 index 195184b1d7..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/BudgetDao.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.BudgetEntity -import java.util.* - -@Dao -interface BudgetDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: BudgetEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("SELECT * FROM budgets WHERE isDeleted = 0 ORDER BY orderId ASC") - suspend fun findAll(): List - - @Query("SELECT * FROM budgets WHERE isSynced = :synced AND isDeleted = :deleted") - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - @Query("SELECT * FROM budgets WHERE id = :id") - suspend fun findById(id: UUID): BudgetEntity? - - @Query("DELETE FROM budgets WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("UPDATE budgets SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query("DELETE FROM budgets") - suspend fun deleteAll() - - @Query("SELECT MAX(orderId) FROM budgets") - suspend fun findMaxOrderNum(): Double? -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/CategoryDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/CategoryDao.kt deleted file mode 100644 index 277c21360a..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/CategoryDao.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.CategoryEntity -import kotlinx.coroutines.flow.Flow -import java.util.* - -@Deprecated("use `:core:persistence`") -@Dao -interface CategoryDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: CategoryEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("SELECT * FROM categories WHERE isDeleted = 0 ORDER BY orderNum ASC") - suspend fun findAllSuspend(): List - - @Query("SELECT * FROM categories WHERE isDeleted = 0 ORDER BY orderNum ASC") - fun findAll(): Flow> - - @Query("SELECT * FROM categories WHERE isSynced = :synced AND isDeleted = :deleted") - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - @Query("SELECT * FROM categories WHERE id = :id") - suspend fun findById(id: UUID): CategoryEntity? - - @Query("DELETE FROM categories WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("UPDATE categories SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query("DELETE FROM categories") - suspend fun deleteAll() - - @Query("SELECT MAX(orderNum) FROM categories") - suspend fun findMaxOrderNum(): Double? - - @Query("SELECT * FROM categories WHERE isDeleted = 0 AND parentCategoryId IS NULL ORDER BY orderNum ASC") - suspend fun findAllParentCategories(): List - - @Query("SELECT * FROM categories WHERE isDeleted = 0 AND parentCategoryId = :id ORDER BY orderNum ASC") - suspend fun findAllSubCategories(id:UUID): List -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/LoanDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/LoanDao.kt deleted file mode 100644 index d072dfc90c..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/LoanDao.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.LoanEntity -import java.util.* - -@Dao -interface LoanDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: LoanEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("SELECT * FROM loans WHERE isDeleted = 0 ORDER BY orderNum ASC") - suspend fun findAll(): List - - @Query("SELECT * FROM loans WHERE isSynced = :synced AND isDeleted = :deleted") - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - @Query("SELECT * FROM loans WHERE id = :id") - suspend fun findById(id: UUID): LoanEntity? - - @Query("DELETE FROM loans WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("UPDATE loans SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query("DELETE FROM loans") - suspend fun deleteAll() - - @Query("SELECT MAX(orderNum) FROM loans") - suspend fun findMaxOrderNum(): Double? -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/LoanRecordDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/LoanRecordDao.kt deleted file mode 100644 index b0c01b911d..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/LoanRecordDao.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.LoanRecordEntity -import java.util.* - -@Dao -interface LoanRecordDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: LoanRecordEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("SELECT * FROM loan_records WHERE isDeleted = 0 ORDER BY dateTime DESC") - suspend fun findAll(): List - - @Query("SELECT * FROM loan_records WHERE isSynced = :synced AND isDeleted = :deleted") - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - @Query("SELECT * FROM loan_records WHERE id = :id") - suspend fun findById(id: UUID): LoanRecordEntity? - - @Query("SELECT * FROM loan_records WHERE loanId = :loanId AND isDeleted = 0 ORDER BY dateTime DESC") - suspend fun findAllByLoanId(loanId: UUID): List - - @Query("DELETE FROM loan_records WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("UPDATE loan_records SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query("DELETE FROM loan_records") - suspend fun deleteAll() -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/PlannedPaymentRuleDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/PlannedPaymentRuleDao.kt deleted file mode 100644 index 23d6492392..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/PlannedPaymentRuleDao.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.PlannedPaymentRuleEntity -import java.util.* - -@Dao -interface PlannedPaymentRuleDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: PlannedPaymentRuleEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("SELECT * FROM planned_payment_rules WHERE isDeleted = 0 ORDER BY amount DESC, startDate ASC") - suspend fun findAll(): List - - @Query("SELECT * FROM planned_payment_rules WHERE isSynced = :synced AND isDeleted = :deleted") - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - @Query("SELECT * FROM planned_payment_rules WHERE isDeleted = 0 AND oneTime = :oneTime ORDER BY amount DESC, startDate ASC") - suspend fun findAllByOneTime(oneTime: Boolean): List - - @Query("SELECT * FROM planned_payment_rules WHERE id = :id AND isDeleted = 0") - suspend fun findById(id: UUID): PlannedPaymentRuleEntity? - - @Query("UPDATE planned_payment_rules SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query("UPDATE planned_payment_rules SET isDeleted = 1, isSynced = 0 WHERE accountId = :accountId") - suspend fun flagDeletedByAccountId(accountId: UUID) - - @Query("DELETE FROM planned_payment_rules WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("DELETE FROM planned_payment_rules") - suspend fun deleteAll() - - @Query("SELECT COUNT(*) FROM planned_payment_rules WHERE isDeleted = 0 ") - suspend fun countPlannedPayments(): Long -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/SettingsDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/SettingsDao.kt deleted file mode 100644 index 4cefea5507..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/SettingsDao.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.SettingsEntity -import kotlinx.coroutines.flow.Flow -import java.util.* - -@Deprecated("replaced with DataStore") -@Dao -interface SettingsDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: SettingsEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("UPDATE settings SET currency = :currencyCode") - suspend fun updateBaseCurrency(currencyCode: String) - - @Query("SELECT * FROM settings LIMIT 1") - suspend fun findFirstSuspend(): SettingsEntity - - @Query("SELECT * FROM settings LIMIT 1") - fun findFirst(): Flow - - @Query("SELECT * FROM settings") - suspend fun findAll(): List - - @Query("SELECT * FROM settings WHERE id = :id") - suspend fun findById(id: UUID): SettingsEntity? - - @Query("DELETE FROM settings WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("DELETE FROM settings") - suspend fun deleteAll() -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/TransactionDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/TransactionDao.kt deleted file mode 100644 index 8f95f0d16b..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/TransactionDao.kt +++ /dev/null @@ -1,226 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.* -import androidx.sqlite.db.SupportSQLiteQuery -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.wallet.io.persistence.data.TransactionEntity -import java.time.LocalDateTime -import java.util.* - -@Deprecated("use `:core:persistence`") -@Dao -interface TransactionDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: TransactionEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(value: List) - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 ORDER BY dateTime DESC, dueDate ASC") - suspend fun findAll(): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 LIMIT 1") - suspend fun findAll_LIMIT_1(): List - - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND type = :type ORDER BY dateTime DESC") - suspend fun findAllByType(type: TrnTypeOld): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND type = :type and accountId = :accountId ORDER BY dateTime DESC") - suspend fun findAllByTypeAndAccount( - type: TrnTypeOld, - accountId: UUID - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND type = :type and accountId = :accountId and dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllByTypeAndAccountBetween( - type: TrnTypeOld, - accountId: UUID, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND type = :type and toAccountId = :toAccountId ORDER BY dateTime DESC") - suspend fun findAllTransfersToAccount( - toAccountId: UUID, - type: TrnTypeOld = TrnTypeOld.TRANSFER - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND type = :type and toAccountId = :toAccountId and dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllTransfersToAccountBetween( - toAccountId: UUID, - startDate: LocalDateTime, - endDate: LocalDateTime, - type: TrnTypeOld = TrnTypeOld.TRANSFER - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND accountId = :accountId AND dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllByAccountAndBetween( - accountId: UUID, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND (categoryId = :categoryId) AND dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllByCategoryAndBetween( - categoryId: UUID, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND (categoryId IS NULL) AND dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllUnspecifiedAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND (categoryId = :categoryId) AND type = :type AND dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllByCategoryAndTypeAndBetween( - categoryId: UUID, - type: TrnTypeOld, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND (categoryId IS NULL) AND type = :type AND dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllUnspecifiedAndTypeAndBetween( - type: TrnTypeOld, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND toAccountId = :toAccountId AND dateTime >= :startDate AND dateTime <= :endDate ORDER BY dateTime DESC") - suspend fun findAllToAccountAndBetween( - toAccountId: UUID, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND dueDate >= :startDate AND dueDate <= :endDate ORDER BY dueDate ASC") - suspend fun findAllDueToBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND dueDate >= :startDate AND dueDate <= :endDate AND (categoryId = :categoryId) ORDER BY dateTime DESC, dueDate ASC") - suspend fun findAllDueToBetweenByCategory( - startDate: LocalDateTime, - endDate: LocalDateTime, - categoryId: UUID - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND dueDate >= :startDate AND dueDate <= :endDate AND (categoryId IS NULL) ORDER BY dateTime DESC, dueDate ASC") - suspend fun findAllDueToBetweenByCategoryUnspecified( - startDate: LocalDateTime, - endDate: LocalDateTime, - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND dueDate >= :startDate AND dueDate <= :endDate AND accountId = :accountId ORDER BY dateTime DESC, dueDate ASC") - suspend fun findAllDueToBetweenByAccount( - startDate: LocalDateTime, - endDate: LocalDateTime, - accountId: UUID - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND recurringRuleId = :recurringRuleId ORDER BY dateTime DESC") - suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND dateTime >= :startDate AND dateTime <= :endDate AND type = :type ORDER BY dateTime DESC") - suspend fun findAllBetweenAndType( - startDate: LocalDateTime, - endDate: LocalDateTime, - type: TrnTypeOld - ): List - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND dateTime >= :startDate AND dateTime <= :endDate AND recurringRuleId = :recurringRuleId ORDER BY dateTime DESC") - suspend fun findAllBetweenAndRecurringRuleId( - startDate: LocalDateTime, - endDate: LocalDateTime, - recurringRuleId: UUID - ): List - - @Query("SELECT * FROM transactions WHERE id = :id") - suspend fun findById(id: UUID): TransactionEntity? - - @Query("SELECT * FROM transactions WHERE isSynced = :synced AND isDeleted = :deleted") - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - @Query("UPDATE transactions SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query("UPDATE transactions SET isDeleted = 1, isSynced = 0 WHERE recurringRuleId = :recurringRuleId AND dateTime IS NULL") - suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) - - @Query("UPDATE transactions SET isDeleted = 1, isSynced = 0 WHERE accountId = :accountId") - suspend fun flagDeletedByAccountId(accountId: UUID) - - @Query("DELETE FROM transactions WHERE id = :id") - suspend fun deleteById(id: UUID) - - @Query("DELETE FROM transactions WHERE accountId = :accountId") - suspend fun deleteAllByAccountId(accountId: UUID) - - @Query("DELETE FROM transactions") - suspend fun deleteAll() - - @Query("SELECT COUNT(*) FROM transactions WHERE isDeleted = 0 AND dateTime IS NOT null") - suspend fun countHappenedTransactions(): Long - - //Smart Title Suggestions - @Query("SELECT * FROM transactions WHERE title LIKE :pattern AND isDeleted = 0") - suspend fun findAllByTitleMatchingPattern(pattern: String): List - - @Query("SELECT COUNT(*) FROM transactions WHERE title LIKE :pattern AND isDeleted = 0") - suspend fun countByTitleMatchingPattern( - pattern: String, - ): Long - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND (categoryId = :categoryId) ORDER BY dateTime DESC") - suspend fun findAllByCategory( - categoryId: UUID, - ): List - - @Query("SELECT COUNT(*) FROM transactions WHERE title LIKE :pattern AND categoryId = :categoryId AND isDeleted = 0") - suspend fun countByTitleMatchingPatternAndCategoryId( - pattern: String, - categoryId: UUID - ): Long - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND accountId = :accountId ORDER BY dateTime DESC") - suspend fun findAllByAccount( - accountId: UUID - ): List - - @Query("SELECT COUNT(*) FROM transactions WHERE title LIKE :pattern AND accountId = :accountId AND isDeleted = 0") - suspend fun countByTitleMatchingPatternAndAccountId( - pattern: String, - accountId: UUID - ): Long - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND loanId = :loanId AND loanRecordId IS NULL") - suspend fun findLoanTransaction( - loanId: UUID - ): TransactionEntity? - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND loanRecordId = :loanRecordId") - suspend fun findLoanRecordTransaction( - loanRecordId: UUID - ): TransactionEntity? - - @Query("SELECT * FROM transactions WHERE isDeleted = 0 AND loanId = :loanId") - suspend fun findAllByLoanId( - loanId: UUID - ): List - - @RawQuery - suspend fun findByQuery(query: SupportSQLiteQuery): List -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/UserDao.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/UserDao.kt deleted file mode 100644 index 081db84a66..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/dao/UserDao.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.ivy.wallet.io.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.ivy.wallet.io.persistence.data.UserEntity -import java.util.* - -@Dao -interface UserDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(user: UserEntity) - - @Query("SELECT * FROM users WHERE id = :userId") - suspend fun findById(userId: UUID): UserEntity? - - @Query("DELETE FROM users") - suspend fun deleteAll() -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/AccountEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/AccountEntity.kt deleted file mode 100644 index acb551ff0b..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/AccountEntity.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.AccountOld -import java.util.* - -@Deprecated("use `:core:persistence`") -@Entity(tableName = "accounts") -data class AccountEntity( - val name: String, - val currency: String? = null, - val color: Int = 0, - val icon: String? = null, - val orderNum: Double = 0.0, - val includeInBalance: Boolean = true, - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): AccountOld = AccountOld( - name = name, - currency = currency, - color = color, - icon = icon, - orderNum = orderNum, - includeInBalance = includeInBalance, - isSynced = isSynced, - isDeleted = isDeleted, - id = id - ) -} - -fun AccountOld.toEntity(): AccountEntity = AccountEntity( - name = name, - currency = currency, - color = color, - icon = icon, - orderNum = orderNum, - includeInBalance = includeInBalance, - isSynced = isSynced, - isDeleted = isDeleted, - id = id -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/BudgetEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/BudgetEntity.kt deleted file mode 100644 index 3239cbb9ac..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/BudgetEntity.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.Budget -import java.util.* - -@Entity(tableName = "budgets") -data class BudgetEntity( - val name: String, - val amount: Double, - - val categoryIdsSerialized: String?, - val accountIdsSerialized: String?, - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - val orderId: Double, - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): Budget = Budget( - name = name, - amount = amount, - categoryIdsSerialized = categoryIdsSerialized, - accountIdsSerialized = accountIdsSerialized, - isSynced = isSynced, - isDeleted = isDeleted, - orderId = orderId, - id = id - ) - - companion object { - fun serialize(ids: List): String { - return ids.joinToString(separator = ",") - } - - fun type(categoriesCount: Int): String { - return when (categoriesCount) { - 0 -> "Total Budget" - 1 -> "Category Budget" - else -> "Multi-Category ($categoriesCount) Budget" - } - } - } - - fun parseCategoryIds(): List { - return parseIdsString(categoryIdsSerialized) - } - - fun parseAccountIds(): List { - return parseIdsString(accountIdsSerialized) - } - - private fun parseIdsString(idsString: String?): List { - return try { - if (idsString == null) return emptyList() - - idsString - .split(",") - .map { UUID.fromString(it) } - } catch (e: Exception) { - e.printStackTrace() - emptyList() - } - } - - - fun validate(): Boolean { - return name.isNotEmpty() && amount > 0.0 - } -} - -fun Budget.toEntity(): BudgetEntity = BudgetEntity( - name = name, - amount = amount, - categoryIdsSerialized = categoryIdsSerialized, - accountIdsSerialized = accountIdsSerialized, - isSynced = isSynced, - isDeleted = isDeleted, - orderId = orderId, - id = id, -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/CategoryEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/CategoryEntity.kt deleted file mode 100644 index a529326098..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/CategoryEntity.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.CategoryOld -import java.util.* - -@Deprecated("use `:core:persistence`") -@Entity(tableName = "categories") -data class CategoryEntity( - val name: String, - val color: Int = 0, - val icon: String? = null, - val orderNum: Double = 0.0, - val parentCategoryId: UUID? = null, - - // TODO: Add CategoryType = Income | Expense | Both - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): CategoryOld = CategoryOld( - name = name, - color = color, - icon = icon, - orderNum = orderNum, - parentCategoryId = parentCategoryId, - isSynced = isSynced, - isDeleted = isDeleted, - id = id - ) -} - -fun CategoryOld.toEntity(): CategoryEntity = CategoryEntity( - name = name, - color = color, - icon = icon, - orderNum = orderNum, - isSynced = isSynced, - isDeleted = isDeleted, - parentCategoryId = parentCategoryId, - id = id -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/LoanEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/LoanEntity.kt deleted file mode 100644 index 8951d85ba6..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/LoanEntity.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.loan.Loan -import com.ivy.data.loan.LoanType -import java.util.* - -@Entity(tableName = "loans") -data class LoanEntity( - val name: String, - val amount: Double, - val type: LoanType, - val color: Int = 0, - val icon: String? = null, - val orderNum: Double = 0.0, - val accountId: UUID? = null, - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): Loan = Loan( - name = name, - amount = amount, - type = type, - color = color, - icon = icon, - orderNum = orderNum, - accountId = accountId, - isSynced = isSynced, - isDeleted = isDeleted, - id = id - ) - - fun humanReadableType(): String { - return if (type == LoanType.BORROW) "BORROWED" else "LENT" - } -} - -fun Loan.toEntity(): LoanEntity = LoanEntity( - name = name, - amount = amount, - type = type, - color = color, - icon = icon, - orderNum = orderNum, - accountId = accountId, - isSynced = isSynced, - isDeleted = isDeleted, - id = id -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/LoanRecordEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/LoanRecordEntity.kt deleted file mode 100644 index 0fb865b681..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/LoanRecordEntity.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.loan.LoanRecord -import java.time.LocalDateTime -import java.util.* - -@Entity(tableName = "loan_records") -data class LoanRecordEntity( - val loanId: UUID, - val amount: Double, - val note: String? = null, - val dateTime: LocalDateTime, - val interest: Boolean = false, - val accountId: UUID? = null, - //This is used store the converted amount for currencies which are different from the loan account currency - val convertedAmount: Double? = null, - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): LoanRecord = LoanRecord( - loanId = loanId, - amount = amount, - note = note, - dateTime = dateTime, - interest = interest, - accountId = accountId, - convertedAmount = convertedAmount, - isSynced = isSynced, - isDeleted = isDeleted, - id = id - ) -} - -fun LoanRecord.toEntity(): LoanRecordEntity = LoanRecordEntity( - loanId = loanId, - amount = amount, - note = note, - dateTime = dateTime, - interest = interest, - accountId = accountId, - convertedAmount = convertedAmount, - isSynced = isSynced, - isDeleted = isDeleted, - id = id -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/PlannedPaymentRuleEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/PlannedPaymentRuleEntity.kt deleted file mode 100644 index 48a2755b6b..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/PlannedPaymentRuleEntity.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.planned.IntervalType -import com.ivy.data.planned.PlannedPaymentRule -import com.ivy.data.transaction.TrnTypeOld -import java.time.LocalDateTime -import java.util.* - -@Entity(tableName = "planned_payment_rules") -data class PlannedPaymentRuleEntity( - val startDate: LocalDateTime?, - val intervalN: Int?, - val intervalType: IntervalType?, - val oneTime: Boolean, - - val type: TrnTypeOld, - val accountId: UUID, - val amount: Double = 0.0, - val categoryId: UUID? = null, - val title: String? = null, - val description: String? = null, - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): PlannedPaymentRule = PlannedPaymentRule( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime, - type = type, - accountId = accountId, - amount = amount, - categoryId = categoryId, - title = title, - description = description, - isSynced = isSynced, - isDeleted = isDeleted, - id = id - ) -} - -fun PlannedPaymentRule.toEntity(): PlannedPaymentRuleEntity = PlannedPaymentRuleEntity( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - oneTime = oneTime, - type = type, - accountId = accountId, - amount = amount, - categoryId = categoryId, - title = title, - description = description, - isSynced = isSynced, - isDeleted = isDeleted, - id = id -) diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/SettingsEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/SettingsEntity.kt deleted file mode 100644 index 38fd892007..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/SettingsEntity.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.Settings -import com.ivy.data.Theme -import java.util.* - -@Entity(tableName = "settings") -data class SettingsEntity( - val theme: Theme, - val currency: String, - val bufferAmount: Double, - val name: String, - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): Settings = Settings( - theme = theme, - baseCurrency = currency, - bufferAmount = bufferAmount.toBigDecimal(), - name = name, - id = id - ) -} - -fun Settings.toEntity(): SettingsEntity = SettingsEntity( - theme = theme, - currency = baseCurrency, - bufferAmount = bufferAmount.toDouble(), - name = name, - id = id, - - isSynced = true, - isDeleted = false -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/TransactionEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/TransactionEntity.kt deleted file mode 100644 index 4bf2a3b6f7..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/TransactionEntity.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import java.time.LocalDateTime -import java.util.* - -@Deprecated("use `:core:persistence`") -@Entity(tableName = "transactions") -data class TransactionEntity( - val accountId: UUID, - val type: TrnTypeOld, - val amount: Double, - val toAccountId: UUID? = null, - val toAmount: Double? = null, - val title: String? = null, - val description: String? = null, - val dateTime: LocalDateTime? = null, - val categoryId: UUID? = null, - val dueDate: LocalDateTime? = null, - - val recurringRuleId: UUID? = null, - - val attachmentUrl: String? = null, - - //This refers to the loan id that is linked with a transaction - val loanId: UUID? = null, - - //This refers to the loan record id that is linked with a transaction - val loanRecordId: UUID? = null, - - val isSynced: Boolean = false, - val isDeleted: Boolean = false, - - @PrimaryKey - val id: UUID = UUID.randomUUID() -) { - fun toDomain(): TransactionOld = TransactionOld( - accountId = accountId, - type = type, - amount = amount.toBigDecimal(), - toAccountId = toAccountId, - toAmount = toAmount?.toBigDecimal() ?: amount.toBigDecimal(), - title = title, - description = description, - dateTime = dateTime, - categoryId = categoryId, - dueDate = dueDate, - recurringRuleId = recurringRuleId, - attachmentUrl = attachmentUrl, - loanId = loanId, - loanRecordId = loanRecordId, - id = id - ) - - fun isIdenticalWith(transaction: TransactionEntity?): Boolean { - if (transaction == null) return false - - //Set isSynced && isDeleted to false so they aren't accounted in the equals check - return this.copy( - isSynced = false, - isDeleted = false - ) == transaction.copy( - isSynced = false, - isDeleted = false - ) - } -} - -fun TransactionOld.toEntity(): TransactionEntity = TransactionEntity( - accountId = accountId, - type = type, - amount = amount.toDouble(), - toAccountId = toAccountId, - toAmount = toAmount.toDouble(), - title = title, - description = description, - dateTime = dateTime, - categoryId = categoryId, - dueDate = dueDate, - recurringRuleId = recurringRuleId, - attachmentUrl = attachmentUrl, - loanId = loanId, - loanRecordId = loanRecordId, - id = id, - - isSynced = true, - isDeleted = false -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/UserEntity.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/data/UserEntity.kt deleted file mode 100644 index dd1551a59a..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/data/UserEntity.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.ivy.wallet.io.persistence.data - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.ivy.data.user.AuthProviderType -import com.ivy.data.user.User -import java.util.* - -@Entity(tableName = "users") -data class UserEntity( - @ColumnInfo(name = "email") - val email: String, - @ColumnInfo(name = "authProviderType") - val authProviderType: AuthProviderType, - @ColumnInfo(name = "firstName") - var firstName: String, - @ColumnInfo(name = "lastName") - val lastName: String?, - @ColumnInfo(name = "profilePicture") - val profilePicture: String?, - @ColumnInfo(name = "color") - val color: Int, - - @ColumnInfo(name = "testUser") - val testUser: Boolean = false, - - @PrimaryKey @ColumnInfo(name = "id") - var id: UUID -) { - fun toDomain(): User = User( - email = email, - authProviderType = authProviderType, - firstName = firstName, - lastName = lastName, - profilePicture = profilePicture, - color = color, - testUser = testUser, - id = id - ) - - fun names(): String = firstName + if (lastName != null) " $lastName" else "" -} - -fun User.toEntity(): UserEntity = UserEntity( - email = email, - authProviderType = authProviderType, - firstName = firstName, - lastName = lastName, - profilePicture = profilePicture, - color = color, - testUser = testUser, - id = id -) \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration105to106_TrnRecurringRules.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration105to106_TrnRecurringRules.kt deleted file mode 100644 index adaeb93d74..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration105to106_TrnRecurringRules.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration105to106_TrnRecurringRules : Migration(105, 106) { - override fun migrate(database: SupportSQLiteDatabase) { - val trnRulesTable = - "CREATE TABLE IF NOT EXISTS `transaction_recurring_rules` (`startDate` INTEGER NOT NULL, `intervalSeconds` INTEGER NOT NULL, `type` TEXT NOT NULL, `accountId` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` TEXT, `title` TEXT, `description` TEXT, `id` TEXT NOT NULL, PRIMARY KEY(`id`))" - database.execSQL(trnRulesTable) - - database.execSQL("ALTER TABLE transactions ADD COLUMN recurringRuleId TEXT") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration106to107_Wishlist.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration106to107_Wishlist.kt deleted file mode 100644 index 29e3af70d5..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration106to107_Wishlist.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration106to107_Wishlist : Migration(106, 107) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE wishlist_items") - database.execSQL("CREATE TABLE IF NOT EXISTS `wishlist_items` (`name` TEXT NOT NULL, `price` REAL NOT NULL, `accountId` TEXT NOT NULL, `categoryId` TEXT, `description` TEXT, `plannedDateTime` INTEGER, `orderNum` REAL NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration107to108_Sync.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration107to108_Sync.kt deleted file mode 100644 index 7d57f07514..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration107to108_Sync.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration107to108_Sync : Migration(107, 108) { - override fun migrate(database: SupportSQLiteDatabase) { - database.addSyncColumns("accounts") - database.addSyncColumns("categories") - database.addSyncColumns("settings") - database.addSyncColumns("transactions") - } - - private fun SupportSQLiteDatabase.addSyncColumns(tableName: String) { - execSQL("ALTER TABLE $tableName ADD COLUMN isSynced INTEGER NOT NULL DEFAULT 0") - execSQL("ALTER TABLE $tableName ADD COLUMN isDeleted INTEGER NOT NULL DEFAULT 0") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration108to109_Users.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration108to109_Users.kt deleted file mode 100644 index f50d8d4189..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration108to109_Users.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration108to109_Users : Migration(108, 109) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `users` (`email` TEXT NOT NULL, `authProviderType` TEXT NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT, `profilePicture` TEXT, `color` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration109to110_PlannedPayments.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration109to110_PlannedPayments.kt deleted file mode 100644 index 58699790be..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration109to110_PlannedPayments.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration109to110_PlannedPayments : Migration(109, 110) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE transaction_recurring_rules") - - database.execSQL("CREATE TABLE IF NOT EXISTS `transaction_recurring_rules` (`startDate` INTEGER, `intervalN` INTEGER, `intervalType` TEXT, `oneTime` INTEGER NOT NULL, `type` TEXT NOT NULL, `accountId` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` TEXT, `title` TEXT, `description` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))") - } - - private fun SupportSQLiteDatabase.addSyncColumns(tableName: String) { - execSQL("ALTER TABLE $tableName ADD COLUMN isSynced INTEGER NOT NULL DEFAULT 0") - execSQL("ALTER TABLE $tableName ADD COLUMN isDeleted INTEGER NOT NULL DEFAULT 0") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration110to111_PlannedPaymentRule.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration110to111_PlannedPaymentRule.kt deleted file mode 100644 index a4503767ae..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration110to111_PlannedPaymentRule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration110to111_PlannedPaymentRule : Migration(110, 111) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE transaction_recurring_rules") - - database.execSQL("CREATE TABLE IF NOT EXISTS `planned_payment_rules` (`startDate` INTEGER, `intervalN` INTEGER, `intervalType` TEXT, `oneTime` INTEGER NOT NULL, `type` TEXT NOT NULL, `accountId` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` TEXT, `title` TEXT, `description` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration111to112_User_testUser.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration111to112_User_testUser.kt deleted file mode 100644 index 1286a5f7f4..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration111to112_User_testUser.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration111to112_User_testUser : Migration(111, 112) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE users ADD COLUMN testUser INTEGER NOT NULL DEFAULT 0") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration112to113_ExchangeRates.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration112to113_ExchangeRates.kt deleted file mode 100644 index 38f3e13ef3..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration112to113_ExchangeRates.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration112to113_ExchangeRates : Migration(112, 113) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `exchange_rates` (`baseCurrency` TEXT NOT NULL, `currency` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`baseCurrency`, `currency`))") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration113to114_Multi_Currency.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration113to114_Multi_Currency.kt deleted file mode 100644 index 6e357a6ce5..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration113to114_Multi_Currency.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration113to114_Multi_Currency : Migration(113, 114) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE accounts ADD COLUMN currency TEXT") - database.execSQL("ALTER TABLE transactions ADD COLUMN toAmount REAL") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration114to115_Category_Account_Icons.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration114to115_Category_Account_Icons.kt deleted file mode 100644 index 1d22bf83f8..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration114to115_Category_Account_Icons.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration114to115_Category_Account_Icons : Migration(114, 115) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE accounts ADD COLUMN icon TEXT") - database.execSQL("ALTER TABLE categories ADD COLUMN icon TEXT") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration115to116_Account_Include_In_Balance.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration115to116_Account_Include_In_Balance.kt deleted file mode 100644 index e79796d46d..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration115to116_Account_Include_In_Balance.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration115to116_Account_Include_In_Balance : Migration(115, 116) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE accounts ADD COLUMN includeInBalance INTEGER NOT NULL DEFAULT 1") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration116to117_SalteEdgeIntgration.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration116to117_SalteEdgeIntgration.kt deleted file mode 100644 index a89871666a..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration116to117_SalteEdgeIntgration.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration116to117_SalteEdgeIntgration : Migration(116, 117) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE accounts ADD COLUMN seAccountId TEXT") - database.execSQL("ALTER TABLE categories ADD COLUMN seCategoryName TEXT") - database.execSQL("ALTER TABLE transactions ADD COLUMN seTransactionId TEXT") - database.execSQL("ALTER TABLE transactions ADD COLUMN seAutoCategoryId TEXT") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration117to118_Budgets.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration117to118_Budgets.kt deleted file mode 100644 index 5b549076f8..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration117to118_Budgets.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration117to118_Budgets : Migration(117, 118) { - override fun migrate(database: SupportSQLiteDatabase) { - val tableName = "budgets" - database.execSQL("CREATE TABLE IF NOT EXISTS `${tableName}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryIdsSerialized` TEXT, `accountIdsSerialized` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `orderId` REAL NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration118to119_Loans.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration118to119_Loans.kt deleted file mode 100644 index ac2dbbcd57..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration118to119_Loans.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration118to119_Loans : Migration(118, 119) { - companion object { - private const val LOANS_TABLE = "loans" - private const val LOAN_RECORDS_TABLE = "loan_records" - } - - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `$LOANS_TABLE` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `type` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))") - - database.execSQL("CREATE TABLE IF NOT EXISTS `$LOAN_RECORDS_TABLE` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))") - } - -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration119to120_LoanTransactions.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration119to120_LoanTransactions.kt deleted file mode 100644 index 7645eaaf51..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration119to120_LoanTransactions.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration119to120_LoanTransactions : Migration(119, 120) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE transactions ADD COLUMN loanId TEXT") - database.execSQL("ALTER TABLE transactions ADD COLUMN loanRecordId TEXT") - - database.execSQL("ALTER TABLE loan_records ADD COLUMN interest INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE loan_records ADD COLUMN accountId TEXT") - database.execSQL("ALTER TABLE loan_records ADD COLUMN convertedAmount REAL") - database.execSQL("ALTER TABLE loans ADD COLUMN accountId TEXT") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration120to121_DropWishlistItem.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration120to121_DropWishlistItem.kt deleted file mode 100644 index e982ef60ac..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration120to121_DropWishlistItem.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration120to121_DropWishlistItem : Migration(120, 121) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE wishlist_items") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration122to123_DoNothing.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration122to123_DoNothing.kt deleted file mode 100644 index 94f5d39d64..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration122to123_DoNothing.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration122to123_DoNothing : Migration(122, 123) { - override fun migrate(database: SupportSQLiteDatabase) { -// database.execSQL("DROP TABLE exchange_rates") - } -} \ No newline at end of file diff --git a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration122to123_SubCategories.kt b/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration122to123_SubCategories.kt deleted file mode 100644 index d242294939..0000000000 --- a/temp-persistence/src/main/java/com/ivy/temp/persistence/migration/Migration122to123_SubCategories.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.ivy.wallet.io.persistence.migration - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration122to123_SubCategories : Migration(122, 123) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE categories ADD COLUMN parentCategoryId TEXT") - } -} \ No newline at end of file diff --git a/transaction-details/.gitignore b/transaction-details/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/transaction-details/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/transaction-details/README.md b/transaction-details/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/transaction-details/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/transaction-details/build.gradle.kts b/transaction-details/build.gradle.kts deleted file mode 100644 index 68283e0650..0000000000 --- a/transaction-details/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -import com.ivy.buildsrc.EventBus -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` -} - -dependencies { - Hilt() - implementation(project(":common")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) - implementation(project(":widgets")) - EventBus() -} \ No newline at end of file diff --git a/transaction-details/src/main/AndroidManifest.xml b/transaction-details/src/main/AndroidManifest.xml deleted file mode 100644 index 8b8f621184..0000000000 --- a/transaction-details/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/transaction-details/src/main/java/com/ivy/transaction_details/EditTransactionScreen.kt b/transaction-details/src/main/java/com/ivy/transaction_details/EditTransactionScreen.kt deleted file mode 100644 index e9e5634d44..0000000000 --- a/transaction-details/src/main/java/com/ivy/transaction_details/EditTransactionScreen.kt +++ /dev/null @@ -1,610 +0,0 @@ -package com.ivy.transaction_details - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.ivy.base.CustomExchangeRateState -import com.ivy.base.EditTransactionDisplayLoan -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.design.util.hideKeyboard - - -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.ui.edit.core.* -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.ui.theme.components.AddPrimaryAttributeButton -import com.ivy.wallet.ui.theme.components.ChangeTransactionTypeModal -import com.ivy.wallet.ui.theme.components.CustomExchangeRateCard -import com.ivy.wallet.ui.theme.modal.DeleteModal -import com.ivy.wallet.ui.theme.modal.ProgressModal -import com.ivy.wallet.ui.theme.modal.edit.* -import com.ivy.wallet.utils.timeNowLocal -import java.time.LocalDateTime -import java.util.* -import kotlin.math.roundToInt - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.EditTransactionScreen() { - val viewModel: EditTransactionViewModel = hiltViewModel() - - val transactionType by viewModel.transactionType.observeAsState() - val initialTitle by viewModel.initialTitle.collectAsState() - val titleSuggestions by viewModel.titleSuggestions.collectAsState() - val currency by viewModel.currency.collectAsState() - val description by viewModel.description.collectAsState() - val dateTime by viewModel.dateTime.collectAsState() - val category by viewModel.category.collectAsState() - val account by viewModel.account.collectAsState() - val toAccount by viewModel.toAccount.collectAsState() - val dueDate by viewModel.dueDate.collectAsState() - val amount by viewModel.amount.collectAsState() - val loanData by viewModel.displayLoanHelper.collectAsState() - val backgroundProcessing by viewModel.backgroundProcessingStarted.collectAsState() - val customExchangeRateState by viewModel.customExchangeRateState.collectAsState() - - val categories by viewModel.categories.collectAsState(emptyList()) - val accounts by viewModel.accounts.collectAsState(emptyList()) - - val hasChanges by viewModel.hasChanges.collectAsState(false) - - val view = com.ivy.core.ui.temp.rootView() - - UI( - transactionType = TrnTypeOld.TRANSFER, - baseCurrency = currency, - initialTitle = initialTitle, - titleSuggestions = titleSuggestions, - description = description, - dateTime = dateTime, - category = category, - account = account, - toAccount = toAccount, - dueDate = dueDate, - amount = amount, - loanData = loanData, - backgroundProcessing = backgroundProcessing, - customExchangeRateState = customExchangeRateState, - - categories = categories, - accounts = accounts, - - hasChanges = hasChanges, - - onTitleChanged = viewModel::onTitleChanged, - onDescriptionChanged = viewModel::onDescriptionChanged, - onAmountChanged = viewModel::onAmountChanged, - onCategoryChanged = viewModel::onCategoryChanged, - onAccountChanged = viewModel::onAccountChanged, - onToAccountChanged = viewModel::onToAccountChanged, - onDueDateChanged = viewModel::onDueDateChanged, - onSetDateTime = viewModel::onSetDateTime, - onSetTransactionType = viewModel::onSetTransactionType, - - onCreateCategory = viewModel::createCategory, - onEditCategory = viewModel::editCategory, - onPayPlannedPayment = viewModel::onPayPlannedPayment, - onSave = { - view.hideKeyboard() - viewModel.save() - }, - onSetHasChanges = viewModel::setHasChanges, - onDelete = viewModel::delete, - onCreateAccount = viewModel::createAccount, - onExchangeRateChanged = { - viewModel.updateExchangeRate(exRate = it) - } - ) -} - -@ExperimentalFoundationApi -@Composable -private fun BoxWithConstraintsScope.UI( - transactionType: TrnTypeOld, - baseCurrency: String, - initialTitle: String?, - titleSuggestions: Set, - description: String?, - category: CategoryOld?, - dateTime: LocalDateTime?, - account: AccountOld?, - toAccount: AccountOld?, - dueDate: LocalDateTime?, - amount: Double, - loanData: EditTransactionDisplayLoan = EditTransactionDisplayLoan(), - backgroundProcessing: Boolean = false, - customExchangeRateState: CustomExchangeRateState, - - categories: List, - accounts: List, - - hasChanges: Boolean = false, - - onTitleChanged: (String?) -> Unit, - onDescriptionChanged: (String?) -> Unit, - onAmountChanged: (Double) -> Unit, - onCategoryChanged: (CategoryOld?) -> Unit, - onAccountChanged: (AccountOld) -> Unit, - onToAccountChanged: (AccountOld) -> Unit, - onDueDateChanged: (LocalDateTime?) -> Unit, - onSetDateTime: (LocalDateTime) -> Unit, - onSetTransactionType: (TrnTypeOld) -> Unit, - - onCreateCategory: (CreateCategoryData) -> Unit, - onEditCategory: (CategoryOld) -> Unit, - onPayPlannedPayment: () -> Unit, - onSave: (closeScreen: Boolean) -> Unit, - onSetHasChanges: (hasChanges: Boolean) -> Unit, - onDelete: () -> Unit, - onCreateAccount: (CreateAccountData) -> Unit, - onExchangeRateChanged: (Double?) -> Unit = { } -) { - var chooseCategoryModalVisible by remember { mutableStateOf(false) } - var categoryModalData: CategoryModalData? by remember { mutableStateOf(null) } - var accountModalData: AccountModalData? by remember { mutableStateOf(null) } - var descriptionModalVisible by remember { mutableStateOf(false) } - var deleteTrnModalVisible by remember { mutableStateOf(false) } - var changeTransactionTypeModalVisible by remember { mutableStateOf(false) } - var amountModalShown by remember { mutableStateOf(false) } - var exchangeRateAmountModalShown by remember { mutableStateOf(false) } - var accountChangeModal by remember { mutableStateOf(false) } - val waitModalVisible by remember(backgroundProcessing) { - mutableStateOf(backgroundProcessing) - } - var selectedAcc by remember(account) { - mutableStateOf(account) - } - - val amountModalId = - remember(customExchangeRateState.exchangeRate) { - UUID.randomUUID() - } - - var titleTextFieldValue by remember(initialTitle) { - mutableStateOf( - TextFieldValue( - initialTitle ?: "" - ) - ) - } - val titleFocus = FocusRequester() - val scrollState = rememberScrollState() - - //This is to scroll the column to the customExchangeCard composable when it is shown - var customExchangeRatePosition by remember { mutableStateOf(0F) } - LaunchedEffect(key1 = customExchangeRateState.showCard) { - val scrollInt = - if (customExchangeRateState.showCard) customExchangeRatePosition.roundToInt() else 0 - scrollState.animateScrollTo(scrollInt) - } - - Column( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - .verticalScroll(scrollState) - ) { - Spacer(Modifier.height(16.dp)) - - Toolbar( - //Setting the transaction type to TransactionType.TRANSFER for transactions associated - // with loan record to hide the ChangeTransactionType Button - type = if (loanData.isLoanRecord) TrnTypeOld.TRANSFER else transactionType, - initialTransactionId = UUID.randomUUID(), - onDeleteTrnModal = { - deleteTrnModalVisible = true - }, - onChangeTransactionTypeModal = { - changeTransactionTypeModalVisible = true - } - ) - - Spacer(Modifier.height(32.dp)) - - Title( - type = transactionType, - titleFocus = titleFocus, - initialTransactionId = UUID.randomUUID(), - - titleTextFieldValue = titleTextFieldValue, - setTitleTextFieldValue = { - titleTextFieldValue = it - }, - suggestions = titleSuggestions, - scrollState = scrollState, - - onTitleChanged = onTitleChanged, - onNext = { - when { - shouldFocusAmount(amount = amount) -> { - amountModalShown = true - } - else -> { - onSave(true) - } - } - } - ) - - if (loanData.loanCaption != null) { - Spacer(modifier = Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = loanData.loanCaption!!, - style = UI.typoSecond.b2.style( - color = UI.colorsInverted.medium, - fontWeight = FontWeight.Normal - ) - ) - } - - if (transactionType != TrnTypeOld.TRANSFER) { - Spacer(Modifier.height(32.dp)) - - Category( - category = category, - onChooseCategory = { - chooseCategoryModalVisible = true - } - ) - } - - Spacer(Modifier.height(32.dp)) - - - if (dueDate != null) { - DueDate(dueDate = dueDate) { -// ivyContext.datePicker( -// initialDate = dueDate.toLocalDate() -// ) { -// onDueDateChanged(it.atTime(12, 0)) -// } - } - - Spacer(Modifier.height(12.dp)) - } - - Description( - description = description, - onAddDescription = { descriptionModalVisible = true }, - onEditDescription = { descriptionModalVisible = true } - ) - - TransactionDateTime( - dateTime = dateTime, - dueDateTime = dueDate, - onEditDate = { -// ivyContext.datePicker( -// initialDate = dateTime?.convertUTCtoLocal()?.toLocalDate() -// ) { date -> -// onSetDateTime( -// getTrueDate( -// date, dateTime?.toLocalTime() -// ?: timeNowLocal().toLocalTime() -// ) -// ) -// } - }, - onEditTime = { -// ivyContext.timePicker { time -> -// onSetDateTime( -// getTrueDate( -// dateTime?.toLocalDate() -// ?: timeNowLocal().toLocalDate(), time -// ) -// ) -// } - } - ) { -// ivyContext.datePicker( -// initialDate = dateTime?.convertUTCtoLocal()?.toLocalDate(), -// ) { date -> -// ivyContext.timePicker { time -> -// onSetDateTime(getTrueDate(date, time)) -// } -// } - } - - if (transactionType == TrnTypeOld.TRANSFER && customExchangeRateState.showCard) { - Spacer(Modifier.height(12.dp)) - CustomExchangeRateCard( - fromCurrencyCode = baseCurrency, - toCurrencyCode = customExchangeRateState.toCurrencyCode ?: baseCurrency, - exchangeRate = customExchangeRateState.exchangeRate, - onRefresh = { - //Set exchangeRate to null to reset - onExchangeRateChanged(null) - }, - modifier = Modifier.onGloballyPositioned { coordinates -> - customExchangeRatePosition = coordinates.positionInParent().y * 0.3f - } - ) { - exchangeRateAmountModalShown = true - } - } - - if (dueDate == null && transactionType != TrnTypeOld.TRANSFER && dateTime == null) { - Spacer(Modifier.height(12.dp)) - - - AddPrimaryAttributeButton( - icon = R.drawable.ic_planned_payments, - text = stringResource(R.string.add_planned_date_payment), - onClick = { - -// nav.navigateTo( -// EditPlanned( -// plannedPaymentRuleId = null, -// type = transactionType, -// amount = amount, -// accountId = account?.id, -// categoryId = category?.id, -// title = titleTextFieldValue.text, -// description = description, -// ) -// ) - } - ) - } - - Spacer(Modifier.height(600.dp)) //scroll hack - } - - EditBottomSheet( - initialTransactionId = UUID.randomUUID(), - type = transactionType, - accounts = accounts, - selectedAccount = account, - toAccount = toAccount, - amount = amount, - currency = baseCurrency, - convertedAmount = customExchangeRateState.convertedAmount, - convertedAmountCurrencyCode = customExchangeRateState.toCurrencyCode, - - ActionButton = { -// if (screen.initialTransactionId != null) { -// //Edit mode -// if (dueDate != null) { -// //due date stuff -// if (hasChanges) { -// //has changes -// ModalSave { -// onSave(false) -// onSetHasChanges(false) -// } -// } else { -// //no changes, pay -// ModalCheck( -// label = if (transactionType == TrnTypeOld.EXPENSE) stringResource( -// R.string.pay -// ) else stringResource(R.string.get) -// ) { -// onPayPlannedPayment() -// } -// } -// } else { -// //normal transaction -// ModalSave { -// onSave(true) -// } -// } -// } else { -// //create new mode -// ModalAdd { -// onSave(true) -// } -// } - }, - - amountModalShown = amountModalShown, - setAmountModalShown = { - amountModalShown = it - }, - - onAmountChanged = { - onAmountChanged(it) - if (shouldFocusCategory(category, transactionType)) { - chooseCategoryModalVisible = true - } else if (shouldFocusTitle(titleTextFieldValue, transactionType)) { - titleFocus.requestFocus() - } - }, - onSelectedAccountChanged = { - if (loanData.isLoan && account?.currency != it.currency) { - selectedAcc = it - accountChangeModal = true - } else - onAccountChanged(it) - }, - onToAccountChanged = onToAccountChanged, - onAddNewAccount = { - accountModalData = AccountModalData( - account = null, - baseCurrency = baseCurrency, - balance = 0.0 - ) - } - ) - - //Modals - ChooseCategoryModal( - visible = chooseCategoryModalVisible, - initialCategory = category, - categories = categories, - showCategoryModal = { categoryModalData = CategoryModalData(it) }, - onCategoryChanged = { - onCategoryChanged(it) - if (shouldFocusTitle(titleTextFieldValue, transactionType)) { - titleFocus.requestFocus() - } else if (shouldFocusAmount(amount = amount)) { - amountModalShown = true - } - }, - dismiss = { - chooseCategoryModalVisible = false - } - ) - - CategoryModal( - modal = categoryModalData, - onCreateCategory = { createData -> - onCreateCategory(createData) - chooseCategoryModalVisible = false - }, - onEditCategory = onEditCategory, - dismiss = { - categoryModalData = null - } - ) - - AccountModal( - modal = accountModalData, - onCreateAccount = onCreateAccount, - onEditAccount = { _, _ -> }, - dismiss = { - accountModalData = null - } - ) - - DescriptionModal( - visible = descriptionModalVisible, - description = description, - onDescriptionChanged = onDescriptionChanged, - dismiss = { - descriptionModalVisible = false - } - ) - - DeleteModal( - visible = deleteTrnModalVisible, - title = stringResource(R.string.confirm_deletion), - description = stringResource(R.string.transaction_confirm_deletion_description), - dismiss = { deleteTrnModalVisible = false } - ) { - onDelete() - } - - ChangeTransactionTypeModal( - visible = changeTransactionTypeModalVisible, - includeTransferType = true, - initialType = transactionType, - dismiss = { - changeTransactionTypeModalVisible = false - } - ) { - onSetTransactionType(it) - } - - DeleteModal( - visible = accountChangeModal, - title = stringResource(R.string.confirm_account_change), - description = stringResource(R.string.confirm_account_change_description), - buttonText = stringResource(R.string.confirm), - iconStart = R.drawable.ic_agreed, - dismiss = { - accountChangeModal = false - } - ) { - selectedAcc?.let { onAccountChanged(it) } - accountChangeModal = false - } - - ProgressModal( - title = stringResource(R.string.confirm_account_change), - description = stringResource(R.string.account_change_recalculating), - visible = waitModalVisible - ) - - AmountModal( - id = amountModalId, - visible = exchangeRateAmountModalShown, - currency = "", - initialAmount = customExchangeRateState.exchangeRate, - dismiss = { exchangeRateAmountModalShown = false }, - decimalCountMax = 4, - onAmountChanged = { - onExchangeRateChanged(it) - } - ) - -} - -private fun shouldFocusCategory( - category: CategoryOld?, - type: TrnTypeOld -): Boolean = category == null && type != TrnTypeOld.TRANSFER - -private fun shouldFocusTitle( - titleTextFieldValue: TextFieldValue, - type: TrnTypeOld -): Boolean = titleTextFieldValue.text.isBlank() && type != TrnTypeOld.TRANSFER - -private fun shouldFocusAmount(amount: Double) = amount == 0.0 - -@ExperimentalFoundationApi -@Preview -@Composable -private fun Preview() { - IvyPreview { - UI( -// screen = EditTransaction(null, TrnTypeOld.EXPENSE), - initialTitle = "", - titleSuggestions = emptySet(), - baseCurrency = "BGN", - dateTime = timeNowLocal(), - description = null, - category = null, - account = AccountOld(name = "phyre", color = Green.toArgb()), - toAccount = null, - amount = 0.0, - dueDate = null, - transactionType = TrnTypeOld.INCOME, - customExchangeRateState = CustomExchangeRateState(), - - categories = emptyList(), - accounts = emptyList(), - - onDueDateChanged = {}, - onCategoryChanged = {}, - onAccountChanged = {}, - onToAccountChanged = {}, - onDescriptionChanged = {}, - onTitleChanged = {}, - onAmountChanged = {}, - - onCreateCategory = { }, - onEditCategory = {}, - onPayPlannedPayment = {}, - onSave = {}, - onSetHasChanges = {}, - onDelete = {}, - onCreateAccount = { }, - onSetDateTime = {}, - onSetTransactionType = {} - ) - } -} \ No newline at end of file diff --git a/transaction-details/src/main/java/com/ivy/transaction_details/EditTransactionViewModel.kt b/transaction-details/src/main/java/com/ivy/transaction_details/EditTransactionViewModel.kt deleted file mode 100644 index b3378c6e41..0000000000 --- a/transaction-details/src/main/java/com/ivy/transaction_details/EditTransactionViewModel.kt +++ /dev/null @@ -1,651 +0,0 @@ -package com.ivy.transaction_details - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ivy.base.CustomExchangeRateState -import com.ivy.base.EditTransactionDisplayLoan -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.frp.test.TestIdlingResource - -import com.ivy.temp.event.AccountsUpdatedEvent -import com.ivy.wallet.domain.action.account.AccountByIdAct -import com.ivy.wallet.domain.action.account.AccountsActOld -import com.ivy.wallet.domain.action.category.CategoriesActOld -import com.ivy.wallet.domain.action.category.CategoryByIdAct -import com.ivy.wallet.domain.action.transaction.TrnByIdAct -import com.ivy.wallet.domain.deprecated.logic.currency.ExchangeRatesLogic -import com.ivy.wallet.domain.deprecated.logic.loantrasactions.LoanTransactionsLogic -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.domain.deprecated.sync.uploader.TransactionUploader -import com.ivy.wallet.io.persistence.SharedPrefs -import com.ivy.wallet.io.persistence.dao.LoanDao -import com.ivy.wallet.io.persistence.dao.SettingsDao -import com.ivy.wallet.io.persistence.dao.TransactionDao -import com.ivy.wallet.io.persistence.data.toEntity -import com.ivy.wallet.utils.* -import com.ivy.widgets.WalletBalanceReceiver -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import org.greenrobot.eventbus.EventBus -import java.time.LocalDateTime -import java.util.* -import javax.inject.Inject - -@HiltViewModel -class EditTransactionViewModel @Inject constructor( - private val loanDao: LoanDao, - private val transactionDao: TransactionDao, - private val settingsDao: SettingsDao, - private val - private val transactionUploader: TransactionUploader, - private val sharedPrefs: SharedPrefs, - private val exchangeRatesLogic: ExchangeRatesLogic, - private val categoryCreator: CategoryCreator, - private val accountCreator: AccountCreator, - private val plannedPaymentsLogic: PlannedPaymentsLogic, - private val smartTitleSuggestionsLogic: SmartTitleSuggestionsLogic, - private val loanTransactionsLogic: LoanTransactionsLogic, - private val accountsAct: AccountsActOld, - private val categoriesAct: CategoriesActOld, - private val trnByIdAct: TrnByIdAct, - private val categoryByIdAct: CategoryByIdAct, - private val accountByIdAct: AccountByIdAct -) : ViewModel() { - - private val _transactionType = MutableLiveData() - val transactionType = _transactionType - - private val _initialTitle = MutableStateFlow(null) - val initialTitle = _initialTitle.readOnly() - - private val _titleSuggestions = MutableStateFlow(emptySet()) - val titleSuggestions = _titleSuggestions.asStateFlow() - - private val _currency = MutableStateFlow("") - val currency = _currency.readOnly() - - private val _description = MutableStateFlow(null) - val description = _description.readOnly() - - private val _dateTime = MutableStateFlow(null) - val dateTime = _dateTime.readOnly() - - private val _dueDate = MutableStateFlow(null) - val dueDate = _dueDate.readOnly() - - private val _accounts = MutableStateFlow>(emptyList()) - val accounts = _accounts.readOnly() - - private val _categories = MutableStateFlow>(emptyList()) - val categories = _categories.readOnly() - - private val _account = MutableStateFlow(null) - val account = _account.readOnly() - - private val _toAccount = MutableStateFlow(null) - val toAccount = _toAccount.readOnly() - - private val _category = MutableStateFlow(null) - val category = _category.readOnly() - - private val _amount = MutableStateFlow(0.0) - val amount = _amount.readOnly() - - private val _hasChanges = MutableStateFlow(false) - val hasChanges = _hasChanges.readOnly() - - private val _displayLoanHelper: MutableStateFlow = - MutableStateFlow(EditTransactionDisplayLoan()) - val displayLoanHelper = _displayLoanHelper.asStateFlow() - - //This is used to when the transaction is associated with a loan/loan record, - // used to indicate the background updating of loan/loanRecord data - private val _backgroundProcessingStarted = MutableStateFlow(false) - val backgroundProcessingStarted = _backgroundProcessingStarted.asStateFlow() - - private val _customExchangeRateState = MutableStateFlow(CustomExchangeRateState()) - val customExchangeRateState = _customExchangeRateState.asStateFlow() - - private var loadedTransaction: TransactionOld? = null - private var editMode = false - - //Used for optimising in updating all loan/loanRecords - private var accountsChanged = false - - var title: String? = null - private lateinit var baseUserCurrency: String - - fun start() { - viewModelScope.launch { - TestIdlingResource.increment() - -// editMode = screen.initialTransactionId != null - - baseUserCurrency = baseCurrency() - - val accounts = accountsAct(Unit) - if (accounts.isEmpty()) { - closeScreen() - return@launch - } - _accounts.value = accounts - - _categories.value = categoriesAct(Unit) - - reset() - -// loadedTransaction = screen.initialTransactionId?.let { -// trnByIdAct(it) -// } ?: TransactionOld( -// accountId = defaultAccountId( -// screen = screen, -// accounts = accounts -// ), -// categoryId = screen.categoryId, -// type = screen.type, -// amount = BigDecimal.ZERO, -// toAmount = BigDecimal.ZERO -// ) - - display(loadedTransaction!!) - - TestIdlingResource.decrement() - } - } - - private suspend fun getDisplayLoanHelper(trans: TransactionOld): EditTransactionDisplayLoan { - if (trans.loanId == null) - return EditTransactionDisplayLoan() - - val loan = - ioThread { loanDao.findById(trans.loanId!!) } ?: return EditTransactionDisplayLoan() - val isLoanRecord = trans.loanRecordId != null - - val loanWarningDescription = if (isLoanRecord) - "Note: This transaction is associated with a Loan Record of Loan : ${loan.name}\n" + - "You are trying to change the account associated with the loan record to an account of different currency" + - "\n The Loan Record will be re-calculated based on today's currency exchanges rates" - else { - "Note: You are trying to change the account associated with the loan: ${loan.name} with an account " + - "of different currency, " + - "\nAll the loan records will be re-calculated based on today's currency exchanges rates " - } - - val loanCaption = - if (isLoanRecord) "* This transaction is associated with a Loan Record of Loan : ${loan.name}" - else "* This transaction is associated with Loan : ${loan.name}" - - return EditTransactionDisplayLoan( - isLoan = true, - isLoanRecord = isLoanRecord, - loanCaption = loanCaption, - loanWarningDescription = loanWarningDescription - ) - } - - private suspend fun defaultAccountId( - accounts: List, - ): UUID { -// val accountId = screen.accountId -// if (accountId != null) { -// return accountId -// } - - val lastSelectedId = sharedPrefs.getString(SharedPrefs.LAST_SELECTED_ACCOUNT_ID, null) - ?.let { UUID.fromString(it) } - if (lastSelectedId != null && ioThread { accounts.find { it.id == lastSelectedId } } != null) { - //use last selected account - return lastSelectedId - } - - return accounts.first().id - } - - private suspend fun display(transaction: TransactionOld) { - this.title = transaction.title - - _transactionType.value = transaction.type - _initialTitle.value = transaction.title - _dateTime.value = transaction.dateTime - _description.value = transaction.description - _dueDate.value = transaction.dueDate - val selectedAccount = accountByIdAct(transaction.accountId)!! - _account.value = selectedAccount - _toAccount.value = transaction.toAccountId?.let { - accountByIdAct(it) - } - _category.value = transaction.categoryId?.let { - categoryByIdAct(it) - } - _amount.value = transaction.amount.toDouble() - - updateCurrency(account = selectedAccount) - - _customExchangeRateState.value = if (transaction.toAccountId == null) - CustomExchangeRateState() - else { - val exchangeRate = transaction.toAmount / transaction.amount - val toAccountCurrency = - _accounts.value.find { acc -> acc.id == transaction.toAccountId }?.currency - CustomExchangeRateState( - showCard = toAccountCurrency != account.value?.currency, - exchangeRate = exchangeRate.toDouble(), - convertedAmount = transaction.toAmount.toDouble(), - toCurrencyCode = toAccountCurrency, - fromCurrencyCode = currency.value - ) - } - - _displayLoanHelper.value = getDisplayLoanHelper(trans = transaction) - } - - private suspend fun updateCurrency(account: AccountOld) { - _currency.value = account.currency ?: baseCurrency() - } - - private suspend fun baseCurrency(): String = - ioThread { settingsDao.findFirstSuspend().currency } - - fun onAmountChanged(newAmount: Double) { - viewModelScope.launch { - loadedTransaction = loadedTransaction().copy( - amount = newAmount.toBigDecimal() - ) - _amount.value = newAmount - updateCustomExchangeRateState(amt = newAmount) - - saveIfEditMode() - } - } - - fun onTitleChanged(newTitle: String?) { - loadedTransaction = loadedTransaction().copy( - title = newTitle - ) - this.title = newTitle - - saveIfEditMode() - - updateTitleSuggestions(newTitle) - } - - private fun updateTitleSuggestions(title: String? = loadedTransaction().title) { - viewModelScope.launch { - TestIdlingResource.increment() - - _titleSuggestions.value = ioThread { - smartTitleSuggestionsLogic.suggest( - title = title, - categoryId = category.value?.id, - accountId = account.value?.id - ) - } - - TestIdlingResource.decrement() - } - } - - fun onDescriptionChanged(newDescription: String?) { - loadedTransaction = loadedTransaction().copy( - description = newDescription - ) - _description.value = newDescription - - saveIfEditMode() - } - - fun onCategoryChanged(newCategory: CategoryOld?) { - loadedTransaction = loadedTransaction().copy( - categoryId = newCategory?.id - ) - _category.value = newCategory - - saveIfEditMode() - - updateTitleSuggestions() - } - - fun onAccountChanged(newAccount: AccountOld) { - viewModelScope.launch { - TestIdlingResource.increment() - - loadedTransaction = loadedTransaction().copy( - accountId = newAccount.id - ) - _account.value = newAccount - - updateCustomExchangeRateState(fromAccount = newAccount) - - viewModelScope.launch { - updateCurrency(account = newAccount) - } - - accountsChanged = true - - //update last selected account - sharedPrefs.putString(SharedPrefs.LAST_SELECTED_ACCOUNT_ID, newAccount.id.toString()) - - saveIfEditMode() - - updateTitleSuggestions() - - TestIdlingResource.decrement() - } - } - - fun onToAccountChanged(newAccount: AccountOld) { - viewModelScope.launch { - loadedTransaction = loadedTransaction().copy( - toAccountId = newAccount.id - ) - _toAccount.value = newAccount - updateCustomExchangeRateState(toAccount = newAccount) - - saveIfEditMode() - } - } - - fun onDueDateChanged(newDueDate: LocalDateTime?) { - loadedTransaction = loadedTransaction().copy( - dueDate = newDueDate - ) - _dueDate.value = newDueDate - - saveIfEditMode() - } - - fun onSetDateTime(newDateTime: LocalDateTime) { - loadedTransaction = loadedTransaction().copy( - dateTime = newDateTime - ) - _dateTime.value = newDateTime - - saveIfEditMode() - } - - fun onSetTransactionType(newTransactionType: TrnTypeOld) { - loadedTransaction = loadedTransaction().copy( - type = newTransactionType - ) - _transactionType.value = newTransactionType - - saveIfEditMode() - } - - - fun onPayPlannedPayment() { - viewModelScope.launch { - TestIdlingResource.increment() - - plannedPaymentsLogic.payOrGet( - transaction = loadedTransaction(), - syncTransaction = false - ) { paidTransaction -> - loadedTransaction = paidTransaction - _dueDate.value = paidTransaction.dueDate - _dateTime.value = paidTransaction.dateTime - - saveIfEditMode( - closeScreen = true - ) - } - - TestIdlingResource.decrement() - } - } - - - fun delete() { - viewModelScope.launch { - TestIdlingResource.increment() - - ioThread { - loadedTransaction?.let { - transactionDao.flagDeleted(it.id) - } - closeScreen() - - loadedTransaction?.let { - transactionUploader.delete(it.id) - } - } - - TestIdlingResource.decrement() - } - } - - fun createCategory(data: CreateCategoryData) { - viewModelScope.launch { - TestIdlingResource.increment() - - categoryCreator.createCategory(data) { - _categories.value = categoriesAct(Unit) - - //Select the newly created category - onCategoryChanged(it) - } - - TestIdlingResource.decrement() - } - } - - fun editCategory(updatedCategory: CategoryOld) { - viewModelScope.launch { - TestIdlingResource.increment() - - categoryCreator.editCategory(updatedCategory) { - _categories.value = categoriesAct(Unit) - } - - TestIdlingResource.decrement() - } - } - - fun createAccount(data: CreateAccountData) { - viewModelScope.launch { - TestIdlingResource.increment() - - accountCreator.createAccount(data) { - EventBus.getDefault().post(AccountsUpdatedEvent()) - _accounts.value = accountsAct(Unit) - } - - TestIdlingResource.decrement() - } - } - - private fun saveIfEditMode(closeScreen: Boolean = false) { - if (editMode) { - _hasChanges.value = true - - save(closeScreen) - } - } - - fun save(closeScreen: Boolean = true) { - if (!validateTransaction()) { - return - } - - viewModelScope.launch { - TestIdlingResource.increment() - - saveInternal(closeScreen = closeScreen) - - TestIdlingResource.decrement() - } - } - - private suspend fun saveInternal(closeScreen: Boolean) { - try { - ioThread { - val amount = amount.value.toBigDecimal() - - loadedTransaction = loadedTransaction().copy( - accountId = account.value?.id ?: error("no accountId"), - toAccountId = toAccount.value?.id, - toAmount = _customExchangeRateState.value.convertedAmount?.toBigDecimal() - ?: amount, - title = title?.trim(), - description = description.value?.trim(), - amount = amount, - type = transactionType.value ?: error("no transaction type"), - dueDate = dueDate.value, - dateTime = when { - loadedTransaction().dateTime == null && - dueDate.value == null -> { - timeNowUTC() - } - else -> loadedTransaction().dateTime - }, - categoryId = category.value?.id, - isSynced = false - ) - - if (loadedTransaction?.loanId != null) { - loanTransactionsLogic.updateAssociatedLoanData( - loadedTransaction!!.copy(), - onBackgroundProcessingStart = { - _backgroundProcessingStarted.value = true - }, - onBackgroundProcessingEnd = { - _backgroundProcessingStarted.value = false - }, - accountsChanged = accountsChanged - ) - - //Reset Counter - accountsChanged = false - } - - transactionDao.save(loadedTransaction().toEntity()) - com.ivy.core.ui.temp.refreshWidget(WalletBalanceReceiver::class.java) - } - - if (closeScreen) { - closeScreen() - - ioThread { - transactionUploader.sync(loadedTransaction()) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun setHasChanges(hasChanges: Boolean) { - _hasChanges.value = hasChanges - } - - private suspend fun transferToAmount( - amount: Double - ): Double? { - if (transactionType.value != TrnTypeOld.TRANSFER) return null - val toCurrency = toAccount.value?.currency ?: baseCurrency() - val fromCurrency = account.value?.currency ?: baseCurrency() - - return exchangeRatesLogic.convertAmount( - baseCurrency = baseCurrency(), - amount = amount, - fromCurrency = fromCurrency, - toCurrency = toCurrency - ) - } - - private fun closeScreen() { - if (nav.backStackEmpty()) { - nav.resetBackStack() -// nav.navigateTo(Main) - } else { - - } - } - - private fun validateTransaction(): Boolean { - if (transactionType.value == TrnTypeOld.TRANSFER && toAccount.value == null) { - return false - } - - if (amount.value == 0.0) { - return false - } - - return true - } - - private fun reset() { - loadedTransaction = null - - _initialTitle.value = null - _description.value = null - _dueDate.value = null - _category.value = null - _hasChanges.value = false - } - - private fun loadedTransaction() = loadedTransaction ?: error("Loaded transaction is null") - - private suspend fun updateCustomExchangeRateState( - toAccount: AccountOld? = null, - fromAccount: AccountOld? = null, - amt: Double? = null, - exchangeRate: Double? = null, - resetRate: Boolean = false - ) { - computationThread { - val toAcc = toAccount ?: _toAccount.value - val fromAcc = fromAccount ?: _account.value - - val toAccCurrencyCode = toAcc?.currency ?: baseUserCurrency - val fromAccCurrencyCode = fromAcc?.currency ?: baseUserCurrency - - if (toAcc == null || fromAcc == null || (toAccCurrencyCode == fromAccCurrencyCode)) { - _customExchangeRateState.value = CustomExchangeRateState() - return@computationThread - } - - val exRate = exchangeRate - ?: if (customExchangeRateState.value.showCard && toAccCurrencyCode == customExchangeRateState.value.toCurrencyCode - && fromAccCurrencyCode == customExchangeRateState.value.fromCurrencyCode && !resetRate - ) - customExchangeRateState.value.exchangeRate - else - exchangeRatesLogic.convertAmount( - baseCurrency = baseUserCurrency, - amount = 1.0, - fromCurrency = fromAccCurrencyCode, - toCurrency = toAccCurrencyCode - ) - - - val amount = amt ?: _amount.value ?: 0.0 - - val customTransferExchangeRateState = CustomExchangeRateState( - showCard = true, - toCurrencyCode = toAccCurrencyCode, - fromCurrencyCode = fromAccCurrencyCode, - exchangeRate = exRate, - convertedAmount = exRate * amount - ) - - _customExchangeRateState.value = customTransferExchangeRateState - uiThread { - saveIfEditMode() - } - } - } - - fun updateExchangeRate(exRate: Double?) { - viewModelScope.launch { - updateCustomExchangeRateState(exchangeRate = exRate, resetRate = exRate == null) - } - } -} \ No newline at end of file diff --git a/pie-charts/.gitignore b/transaction/.gitignore similarity index 100% rename from pie-charts/.gitignore rename to transaction/.gitignore diff --git a/budgets/README.md b/transaction/README.md similarity index 100% rename from budgets/README.md rename to transaction/README.md diff --git a/ui-components-old/build.gradle.kts b/transaction/build.gradle.kts similarity index 61% rename from ui-components-old/build.gradle.kts rename to transaction/build.gradle.kts index 1f8d2c000d..cd0bdd5721 100644 --- a/ui-components-old/build.gradle.kts +++ b/transaction/build.gradle.kts @@ -1,28 +1,20 @@ import com.ivy.buildsrc.Hilt -import com.ivy.buildsrc.ThirdParty +import com.ivy.buildsrc.Testing apply() plugins { `android-library` - `kotlin-android` -} - -android { - buildFeatures { - compose = true - } } dependencies { Hilt() implementation(project(":common:main")) implementation(project(":design-system")) - implementation(project(":core:data-model")) - implementation(project(":app-base")) + implementation(project(":core:domain")) implementation(project(":core:ui")) - implementation(project(":temp-domain")) + implementation(project(":core:data-model")) + implementation(project(":core:persistence")) implementation(project(":navigation")) - - ThirdParty() + Testing() } \ No newline at end of file diff --git a/transaction/src/main/AndroidManifest.xml b/transaction/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0a097ab0d7 --- /dev/null +++ b/transaction/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/action/TitleSuggestionsFlow.kt b/transaction/src/main/java/com/ivy/transaction/action/TitleSuggestionsFlow.kt new file mode 100644 index 0000000000..a99ac5ce94 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/action/TitleSuggestionsFlow.kt @@ -0,0 +1,50 @@ +package com.ivy.transaction.action + +import arrow.core.nonEmptyListOf +import com.ivy.common.toUUID +import com.ivy.core.domain.action.FlowAction +import com.ivy.core.domain.action.transaction.TrnQuery +import com.ivy.core.domain.action.transaction.TrnQuery.ByCategoryId +import com.ivy.core.domain.action.transaction.TrnsFlow +import com.ivy.core.domain.action.transaction.and +import com.ivy.core.ui.data.CategoryUi +import com.ivy.data.transaction.TrnPurpose +import com.ivy.transaction.pure.suggestTitle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class TitleSuggestionsFlow @Inject constructor( + private val trnsFlow: TrnsFlow, +) : FlowAction>() { + data class Input( + val title: String?, + val categoryUi: CategoryUi?, + val transfer: Boolean, + ) + + override fun Input.createFlow(): Flow> = + trnsFlow(suggestionsQuery()).map { trns -> + suggestTitle( + transactions = trns, + title = title, + ) + } + + private fun Input.suggestionsQuery(): TrnQuery = + if (transfer) { + ByCategoryId( + categoryUi?.id?.toUUID() + ) and TrnQuery.ByPurposeIn( + nonEmptyListOf( + TrnPurpose.Fee, + TrnPurpose.TransferFrom, + TrnPurpose.TransferTo, + ) + ) + } else { + ByCategoryId( + categoryUi?.id?.toUUID() + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/BaseBottomSheet.kt b/transaction/src/main/java/com/ivy/transaction/component/BaseBottomSheet.kt new file mode 100644 index 0000000000..4146f5c7aa --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/BaseBottomSheet.kt @@ -0,0 +1,115 @@ +package com.ivy.transaction.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +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.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.H1 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerWeight +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.DeleteButton +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.keyboardShiftAnimated +import com.ivy.resources.R + +@Composable +fun BoxScope.BaseBottomSheet( + ctaText: String, + @DrawableRes + ctaIcon: Int, + modifier: Modifier = Modifier, + secondaryActions: (@Composable RowScope.() -> Unit)? = null, + onCtaClick: () -> Unit, + content: @Composable ColumnScope.() -> Unit, +) { + val keyboardShiftDp by keyboardShiftAnimated() + Column( + modifier = modifier + .align(Alignment.BottomCenter) + .border(1.dp, UI.colors.neutral, UI.shapes.roundedTop) + .background(UI.colors.pure, UI.shapes.roundedTop) + .padding(bottom = 8.dp, top = 12.dp) + .padding(bottom = keyboardShiftDp) + ) { + content() + BottomBar( + ctaText = ctaText, + ctaIcon = ctaIcon, + secondaryActions = secondaryActions, + onCtaClick = onCtaClick, + ) + } +} + +@Composable +private fun BottomBar( + modifier: Modifier = Modifier, + ctaText: String, + @DrawableRes + ctaIcon: Int, + secondaryActions: (@Composable RowScope.() -> Unit)?, + onCtaClick: () -> Unit +) { + val lineColor = UI.colors.medium + Row( + modifier = modifier + .fillMaxWidth() + .drawBehind { + val height = this.size.height + val width = this.size.width + + drawLine( + color = lineColor, + strokeWidth = 2.dp.toPx(), + start = Offset(x = 0f, y = height / 2), + end = Offset(x = width, y = height / 2) + ) + } + .padding(horizontal = 16.dp) + ) { + SpacerWeight(weight = 1f) + secondaryActions?.invoke(this) + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Focused, + feeling = Feeling.Positive, + text = ctaText, + icon = ctaIcon, + onClick = onCtaClick + ) + } +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + BaseBottomSheet( + ctaText = "CTA", + ctaIcon = R.drawable.ic_round_add_24, + secondaryActions = { + DeleteButton { + + } + SpacerHor(width = 12.dp) + }, + onCtaClick = {}, + ) { + H1(text = "Content") + } + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/CategoryComponent.kt b/transaction/src/main/java/com/ivy/transaction/component/CategoryComponent.kt new file mode 100644 index 0000000000..5ebf36a721 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/CategoryComponent.kt @@ -0,0 +1,114 @@ +package com.ivy.transaction.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.icon.IconSize +import com.ivy.core.ui.icon.ItemIcon +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.rememberContrast +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.transaction.R + +@Composable +internal fun CategoryComponent( + category: CategoryUi?, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + if (category != null) { + CategoryButton( + modifier = modifier, + category = category, + onClick = onClick, + ) + } else { + AddCategoryButton( + modifier = modifier, + onClick = onClick + ) + } +} + +@Composable +private fun CategoryButton( + category: CategoryUi, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Row( + modifier = modifier + .clip(UI.shapes.rounded) + .background(category.color, UI.shapes.rounded) + .clickable(onClick = onClick) + .padding(start = 16.dp, end = 24.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val contrast = rememberContrast(category.color) + ItemIcon( + itemIcon = category.icon, + size = IconSize.S, + tint = contrast + ) + SpacerHor(width = 12.dp) + B2(text = category.name, color = contrast) + } +} + +@Composable +private fun AddCategoryButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = stringResource(R.string.add_category), + icon = R.drawable.ic_round_add_24, + onClick = onClick + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview_NoCategory() { + ComponentPreview { + CategoryComponent( + category = null, + onClick = {} + ) + } +} + +@Preview +@Composable +private fun Preview_WithCategory() { + ComponentPreview { + CategoryComponent( + category = dummyCategoryUi(), + onClick = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/DescriptionComponent.kt b/transaction/src/main/java/com/ivy/transaction/component/DescriptionComponent.kt new file mode 100644 index 0000000000..ce4ea602c1 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/DescriptionComponent.kt @@ -0,0 +1,100 @@ +package com.ivy.transaction.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.transaction.R + +@Composable +internal fun DescriptionComponent( + description: String?, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + if (description != null) { + Description( + modifier = modifier, + description = description, + onClick = onClick, + ) + } else { + AddDescriptionButton( + modifier = modifier, + onClick = onClick + ) + } +} + +@Composable +private fun Description( + description: String, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + B2( + modifier = modifier + .fillMaxWidth() + .clip(UI.shapes.rounded) + .border(1.dp, UI.colors.neutral, UI.shapes.rounded) + .clickable(onClick = onClick) + .padding(all = 16.dp), + text = description, + fontWeight = FontWeight.Normal + ) +} + +@Composable +private fun AddDescriptionButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = stringResource(R.string.add_description), + icon = R.drawable.ic_round_add_24, + onClick = onClick + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview_NoDescription() { + ComponentPreview { + DescriptionComponent( + description = null, + onClick = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Description() { + ComponentPreview { + DescriptionComponent( + description = "Description\nand more\nand more\ntesting.", + onClick = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/FeeComponent.kt b/transaction/src/main/java/com/ivy/transaction/component/FeeComponent.kt new file mode 100644 index 0000000000..f68d155b74 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/FeeComponent.kt @@ -0,0 +1,72 @@ +package com.ivy.transaction.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.design.l0_system.UI +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.resources.R + +@Composable +internal fun FeeComponent( + fee: ValueUi, + validFee: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + if (!validFee) { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Negative, + text = "Add fee", + icon = R.drawable.ic_custom_bills_s, + onClick = onClick + ) + } else { + IvyButton( + modifier = modifier, + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Negative, + text = "Fee ${fee.amount} ${fee.currency}", + typo = UI.typoSecond.b2, + icon = R.drawable.ic_custom_bills_s, + onClick = onClick + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview_NoFee() { + ComponentPreview { + FeeComponent( + fee = dummyValueUi(), + validFee = false, + onClick = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Fee() { + ComponentPreview { + FeeComponent( + fee = dummyValueUi(amount = "2"), + validFee = true, + onClick = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/TitleInput.kt b/transaction/src/main/java/com/ivy/transaction/component/TitleInput.kt new file mode 100644 index 0000000000..e2a220bf35 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/TitleInput.kt @@ -0,0 +1,50 @@ +package com.ivy.transaction.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l2_components.input.InputFieldType +import com.ivy.design.l2_components.input.IvyInputField +import com.ivy.design.util.ComponentPreview + +@Composable +internal fun TitleInput( + title: String?, + focus: FocusRequester, + modifier: Modifier = Modifier, + onTitleChange: (String) -> Unit, + onCta: () -> Unit, +) { + IvyInputField( + modifier = modifier + .focusRequester(focus) + .fillMaxWidth() + .padding(horizontal = 16.dp), + type = InputFieldType.SingleLine, + initialValue = title ?: "", + placeholder = "Title", + imeAction = ImeAction.Done, + onImeAction = { onCta() }, + onValueChange = onTitleChange, + ) +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + TitleInput( + title = "Title", + focus = FocusRequester(), + onTitleChange = {}, + onCta = {}, + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/TitleSuggestions.kt b/transaction/src/main/java/com/ivy/transaction/component/TitleSuggestions.kt new file mode 100644 index 0000000000..5f82b3ce76 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/TitleSuggestions.kt @@ -0,0 +1,83 @@ +package com.ivy.transaction.component + +import androidx.compose.animation.* +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.B2Second +import com.ivy.design.util.ComponentPreview + +@Composable +internal fun TitleSuggestions( + focused: Boolean, + suggestions: List, + modifier: Modifier = Modifier, + onSuggestionClick: (String) -> Unit, +) { + AnimatedVisibility( + modifier = modifier + .padding(horizontal = 16.dp), + visible = focused && suggestions.isNotEmpty(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .border(1.dp, UI.colors.primary, UI.shapes.rounded) + .padding(vertical = 4.dp), + ) { + suggestions.forEachIndexed { index, suggestion -> + key(index.toString() + suggestion) { + Suggestion(suggestion = suggestion) { + onSuggestionClick(suggestion) + } + } + } + } + } +} + +@Composable +private fun Suggestion( + suggestion: String, + onClick: () -> Unit, +) { + B2Second( + modifier = Modifier + .fillMaxWidth() + .clip(UI.shapes.rounded) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 12.dp), + text = suggestion, + fontWeight = FontWeight.Normal, + ) +} + + +@Preview +@Composable +private fun TitleSuggestionsPreview() { + ComponentPreview { + TitleSuggestions( + focused = true, + suggestions = listOf( + "Suggestion 1", + "Suggestion 2", + "Suggestion 3", + ), + onSuggestionClick = {}, + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/TransactionBottomSheet.kt b/transaction/src/main/java/com/ivy/transaction/component/TransactionBottomSheet.kt new file mode 100644 index 0000000000..62600370e1 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/TransactionBottomSheet.kt @@ -0,0 +1,217 @@ +package com.ivy.transaction.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.dummy.dummyValue +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.account.AccountButton +import com.ivy.core.ui.account.create.CreateAccountModal +import com.ivy.core.ui.account.pick.SingleAccountPickerModal +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.value.AmountCurrencyBig +import com.ivy.core.ui.value.AmountCurrencySmall +import com.ivy.data.Value +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview +import com.ivy.resources.R +import com.ivy.transaction.modal.AmountModalWithAccounts + +@Composable +internal fun BoxScope.AmountAccountSheet( + amountUi: ValueUi, + amount: Value, + amountBaseCurrency: ValueUi?, + account: AccountUi, + ctaText: String, + @DrawableRes + ctaIcon: Int, + accountPickerModal: IvyModal, + amountModal: IvyModal, + modifier: Modifier = Modifier, + secondaryActions: (@Composable RowScope.() -> Unit)? = null, + onAccountChange: (AccountUi) -> Unit, + onAmountEnter: (Value) -> Unit, + onCtaClick: () -> Unit, +) { + BaseBottomSheet( + modifier = modifier, + ctaText = ctaText, + ctaIcon = ctaIcon, + onCtaClick = onCtaClick, + secondaryActions = secondaryActions, + ) { + AmountAccountRow( + amount = amountUi, + amountBaseCurrency = amountBaseCurrency, + account = account, + onAmountClick = { + amountModal.show() + }, + onAccountClick = { + accountPickerModal.show() + } + ) + SpacerVer(height = 16.dp) + } + + Modals( + account = account, + accountPickerModal = accountPickerModal, + amount = amount, + amountModal = amountModal, + onAccountChange = onAccountChange, + onAmountEnter = onAmountEnter, + ) +} + +@Composable +private fun AmountAccountRow( + amount: ValueUi, + amountBaseCurrency: ValueUi?, + account: AccountUi, + modifier: Modifier = Modifier, + onAmountClick: () -> Unit, + onAccountClick: () -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 8.dp) + .clickable(onClick = onAmountClick) + .padding(start = 8.dp), + ) { + AmountCurrencyBig(value = amount) + if (amountBaseCurrency != null) { + SpacerVer(height = 4.dp) + Row { + AmountCurrencySmall( + value = amountBaseCurrency, + color = UI.colors.primary, + ) + } + } + } + AccountButton( + account = account, + onClick = onAccountClick + ) + } +} + +@Composable +private fun BoxScope.Modals( + account: AccountUi, + accountPickerModal: IvyModal, + amount: Value, + amountModal: IvyModal, + onAccountChange: (AccountUi) -> Unit, + onAmountEnter: (Value) -> Unit, +) { + SingleAccountPickerModal( + modal = accountPickerModal, + selected = account, + onSelectAccount = onAccountChange + ) + + val createAccountModal = rememberIvyModal() + AmountModalWithAccounts( + modal = amountModal, + amount = amount, + account = account, + onAddAccount = { + createAccountModal.show() + }, + onAmountEnter = onAmountEnter, + onAccountChange = onAccountChange + ) + + CreateAccountModal( + modal = createAccountModal, + level = 2, + ) +} + +// region Preview +@Preview +@Composable +private fun Preview() { + IvyPreview { + AmountAccountSheet( + amountUi = dummyValueUi(), + amount = dummyValue(), + amountBaseCurrency = null, + account = dummyAccountUi(), + ctaText = "Add", + ctaIcon = R.drawable.ic_round_add_24, + accountPickerModal = rememberIvyModal(), + amountModal = rememberIvyModal(), + onAccountChange = {}, + onAmountEnter = {}, + onCtaClick = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_LongAmount() { + IvyPreview { + AmountAccountSheet( + amountUi = dummyValueUi(amount = "12345678901234567890.33"), + amount = dummyValue(), + amountBaseCurrency = dummyValueUi( + amount = "12345678901234567890.33", + currency = "BGN", + ), + account = dummyAccountUi(), + ctaText = "Add", + ctaIcon = R.drawable.ic_round_add_24, + accountPickerModal = rememberIvyModal(), + amountModal = rememberIvyModal(), + onAccountChange = {}, + onAmountEnter = {}, + onCtaClick = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_LongAmount_LongAccount() { + IvyPreview { + AmountAccountSheet( + amountUi = dummyValueUi(amount = "12345678901234567890.33"), + amount = dummyValue(), + amountBaseCurrency = dummyValueUi(), + account = dummyAccountUi( + name = "Revolut Business Company 2 Account" + ), + ctaText = "Add", + ctaIcon = R.drawable.ic_round_add_24, + accountPickerModal = rememberIvyModal(), + amountModal = rememberIvyModal(), + onAccountChange = {}, + onAmountEnter = {}, + onCtaClick = {}, + ) + } +} + +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/TransferBottomSheet.kt b/transaction/src/main/java/com/ivy/transaction/component/TransferBottomSheet.kt new file mode 100644 index 0000000000..0a5dc0f373 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/TransferBottomSheet.kt @@ -0,0 +1,322 @@ +package com.ivy.transaction.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.dummy.dummyValue +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.account.AccountButton +import com.ivy.core.ui.account.create.CreateAccountModal +import com.ivy.core.ui.account.pick.SingleAccountPickerModal +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.value.AmountCurrencyBig +import com.ivy.data.Value +import com.ivy.design.l0_system.color.Blue2Dark +import com.ivy.design.l1_buildingBlocks.B1 +import com.ivy.design.l1_buildingBlocks.IconRes +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview +import com.ivy.transaction.R +import com.ivy.transaction.modal.AmountModalWithAccounts + +@Composable +fun BoxScope.TransferBottomSheet( + accountFrom: AccountUi, + amountFromUi: ValueUi, + amountFrom: Value, + accountTo: AccountUi, + amountToUi: ValueUi, + amountTo: Value, + + modifier: Modifier = Modifier, + secondaryActions: (@Composable RowScope.() -> Unit)? = null, + ctaText: String, + @DrawableRes + ctaIcon: Int, + onCtaClick: () -> Unit, + onFromAccountChange: (AccountUi) -> Unit, + onToAccountChange: (AccountUi) -> Unit, + onFromAmountChange: (Value) -> Unit, + onToAmountChange: (Value) -> Unit, +) { + val fromAccountPickerModal = rememberIvyModal() + val toAccountPickerModal = rememberIvyModal() + val fromAmountModal = rememberIvyModal() + val toAmountModal = rememberIvyModal() + + BaseBottomSheet( + modifier = modifier, + ctaText = ctaText, + ctaIcon = ctaIcon, + secondaryActions = secondaryActions, + onCtaClick = onCtaClick + ) { + TransferContent( + accountFrom = accountFrom, + amountFrom = amountFromUi, + accountTo = accountTo, + amountTo = amountToUi, + + onFromAccountClick = { + fromAccountPickerModal.show() + }, + onToAccountClick = { + toAccountPickerModal.show() + }, + onFromAmountClick = { + fromAmountModal.show() + }, + onToAmountClick = { + toAmountModal.show() + }, + ) + SpacerVer(height = 16.dp) + } + + Modals( + accountFrom = accountFrom, + amountFrom = amountFrom, + accountTo = accountTo, + amountTo = amountTo, + fromAccountPickerModal = fromAccountPickerModal, + toAccountPickerModal = toAccountPickerModal, + fromAmountModal = fromAmountModal, + toAmountModal = toAmountModal, + onFromAccountChange = onFromAccountChange, + onToAccountChange = onToAccountChange, + onFromAmountChange = onFromAmountChange, + onToAmountChange = onToAmountChange, + ) +} + +@Composable +private fun TransferContent( + accountFrom: AccountUi, + amountFrom: ValueUi, + accountTo: AccountUi, + amountTo: ValueUi, + + onFromAccountClick: () -> Unit, + onFromAmountClick: () -> Unit, + onToAccountClick: () -> Unit, + onToAmountClick: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + AccountAmount( + modifier = Modifier.weight(1f), + label = "From", + alignment = Alignment.Start, + account = accountFrom, + amount = amountFrom, + onAccountClick = onFromAccountClick, + onAmountClick = onFromAmountClick, + ) + IconRes(icon = R.drawable.ic_arrow_right) + AccountAmount( + modifier = Modifier.weight(1f), + label = "To", + alignment = Alignment.End, + account = accountTo, + amount = amountTo, + onAccountClick = onToAccountClick, + onAmountClick = onToAmountClick, + ) + } +} + +@Composable +private fun BoxScope.Modals( + accountFrom: AccountUi, + amountFrom: Value, + accountTo: AccountUi, + amountTo: Value, + + fromAccountPickerModal: IvyModal, + toAccountPickerModal: IvyModal, + fromAmountModal: IvyModal, + toAmountModal: IvyModal, + + onFromAccountChange: (AccountUi) -> Unit, + onToAccountChange: (AccountUi) -> Unit, + onFromAmountChange: (Value) -> Unit, + onToAmountChange: (Value) -> Unit, +) { + // From + SingleAccountPickerModal( + modal = fromAccountPickerModal, + selected = accountFrom, + onSelectAccount = onFromAccountChange + ) + // To + SingleAccountPickerModal( + modal = toAccountPickerModal, + selected = accountTo, + onSelectAccount = onToAccountChange + ) + + val createAccountModal = rememberIvyModal() + // From + AmountModalWithAccounts( + modal = fromAmountModal, + key = "from", + amount = amountFrom, + account = accountFrom, + onAddAccount = { + createAccountModal.show() + }, + onAmountEnter = onFromAmountChange, + onAccountChange = onFromAccountChange + ) + // To + AmountModalWithAccounts( + modal = toAmountModal, + key = "to", + amount = amountTo, + account = accountTo, + onAddAccount = { + createAccountModal.show() + }, + onAmountEnter = onToAmountChange, + onAccountChange = onToAccountChange + ) + + CreateAccountModal( + modal = createAccountModal, + level = 2, + ) +} + +@Composable +private fun AccountAmount( + account: AccountUi, + amount: ValueUi, + label: String, + alignment: Alignment.Horizontal, + modifier: Modifier = Modifier, + onAccountClick: () -> Unit, + onAmountClick: () -> Unit, +) { + Column( + modifier = modifier.padding(horizontal = 12.dp), + horizontalAlignment = alignment + ) { + B1( + modifier = Modifier.padding(horizontal = 16.dp), + text = label, + fontWeight = FontWeight.SemiBold, + ) + SpacerVer(height = 8.dp) + AccountButton(account = account, onClick = onAccountClick) + Column( + modifier = Modifier + .clickable(onClick = onAmountClick) + .padding(horizontal = 12.dp) + .padding(top = 8.dp), + horizontalAlignment = alignment, + ) { + AmountCurrencyBig(value = amount) + } + } +} + + +// region Previews +@Preview +@Composable +private fun Preview() { + IvyPreview { + TransferBottomSheet( + accountFrom = dummyAccountUi(), + amountFromUi = dummyValueUi(), + accountTo = dummyAccountUi(), + amountToUi = dummyValueUi(), + amountTo = dummyValue(), // used only for modals + amountFrom = dummyValue(), // used only for modals + ctaText = "Add", + ctaIcon = R.drawable.ic_round_add_24, + onCtaClick = {}, + onFromAccountChange = {}, + onToAccountChange = {}, + onFromAmountChange = {}, + onToAmountChange = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_Normal() { + IvyPreview { + TransferBottomSheet( + accountFrom = dummyAccountUi( + name = "DSK Bank", + color = Blue2Dark, + ), + amountFromUi = dummyValueUi( + amount = "400" + ), + accountTo = dummyAccountUi( + name = "Cash", + ), + amountToUi = dummyValueUi( + amount = "400" + ), + amountTo = dummyValue(), // used only for modals + amountFrom = dummyValue(), // used only for modals + ctaText = "Add", + ctaIcon = R.drawable.ic_round_add_24, + onCtaClick = {}, + onFromAccountChange = {}, + onToAccountChange = {}, + onFromAmountChange = {}, + onToAmountChange = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_LongMultiCurrency() { + IvyPreview { + TransferBottomSheet( + accountFrom = dummyAccountUi( + name = "Revolut Business EUR", + color = Blue2Dark, + ), + amountFromUi = dummyValueUi( + amount = "160,235.30", + currency = "EUR" + ), + accountTo = dummyAccountUi( + name = "Bank Company Account BGN", + ), + amountToUi = dummyValueUi( + amount = "310,818.94", + currency = "BGN", + ), + amountTo = dummyValue(), // used only for modals + amountFrom = dummyValue(), // used only for modals + ctaText = "Add", + ctaIcon = R.drawable.ic_round_add_24, + onCtaClick = {}, + onFromAccountChange = {}, + onToAccountChange = {}, + onFromAmountChange = {}, + onToAmountChange = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/TransferRateComponent.kt b/transaction/src/main/java/com/ivy/transaction/component/TransferRateComponent.kt new file mode 100644 index 0000000000..f456c4031a --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/TransferRateComponent.kt @@ -0,0 +1,48 @@ +package com.ivy.transaction.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.design.l0_system.UI +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.resources.R +import com.ivy.transaction.data.TransferRateUi + +@Composable +fun TransferRateComponent( + rate: TransferRateUi, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + IvyButton( + modifier = modifier, + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = Feeling.Custom(UI.colors.redP1), + text = "${rate.fromCurrency}-${rate.toCurrency}: ${rate.rateValueFormatted}", + icon = R.drawable.round_currency_exchange_24, + typo = UI.typoSecond.b2, + onClick = onClick, + ) +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + TransferRateComponent( + rate = TransferRateUi( + rateValueFormatted = "1.2", + rateValue = 1.2, + fromCurrency = "EUR", + toCurrency = "USD", + ), + onClick = {} + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/TrnScreenToolbar.kt b/transaction/src/main/java/com/ivy/transaction/component/TrnScreenToolbar.kt new file mode 100644 index 0000000000..d1f97b24b1 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/TrnScreenToolbar.kt @@ -0,0 +1,56 @@ +package com.ivy.transaction.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l1_buildingBlocks.SpacerWeight +import com.ivy.design.l2_components.modal.CloseButton +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview + +@Composable +internal fun TrnScreenToolbar( + onClose: () -> Unit, + actions: @Composable RowScope.() -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CloseButton(onClick = onClose) + SpacerWeight(weight = 1f) + actions() + } +} + + +@Preview +@Composable +private fun Preview() { + ComponentPreview { + TrnScreenToolbar( + onClose = {}, + actions = { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + text = "Test", + icon = null, + onClick = {} + ) + } + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/component/TrnTimeComponent.kt b/transaction/src/main/java/com/ivy/transaction/component/TrnTimeComponent.kt new file mode 100644 index 0000000000..77e27f415c --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/component/TrnTimeComponent.kt @@ -0,0 +1,97 @@ +package com.ivy.transaction.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.color.Orange +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.ComponentPreview +import com.ivy.transaction.R + +@Composable +internal fun TrnTimeComponent( + extendedTrnTime: TrnTimeUi, + modifier: Modifier = Modifier, + onDateClick: () -> Unit, + onTimeClick: () -> Unit, +) { + val feeling = when (extendedTrnTime) { + is TrnTimeUi.Actual -> Feeling.Positive + is TrnTimeUi.Due -> Feeling.Custom(Orange) + } + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + IvyButton( + modifier = Modifier.weight(1.5f), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = feeling, + text = when (extendedTrnTime) { + is TrnTimeUi.Actual -> extendedTrnTime.actualDate + is TrnTimeUi.Due -> extendedTrnTime.dueOnDate + }, + icon = R.drawable.ic_round_calendar_month_24, + typo = UI.typoSecond.b2, + onClick = onDateClick + ) + SpacerHor(width = 8.dp) + IvyButton( + modifier = Modifier.weight(1f), + size = ButtonSize.Big, + visibility = Visibility.Medium, + feeling = feeling, + text = when (extendedTrnTime) { + is TrnTimeUi.Actual -> extendedTrnTime.actualTime + is TrnTimeUi.Due -> extendedTrnTime.dueOnTime + }, + icon = R.drawable.round_time_24, + typo = UI.typoSecond.b2, + onClick = onTimeClick + ) + } +} + + +// region Preview +@Preview +@Composable +private fun Preview_Actual() { + ComponentPreview { + TrnTimeComponent( + extendedTrnTime = TrnTimeUi.Actual( + actualDate = "Dec 21, 2021", + actualTime = "21:46", + ), + onDateClick = {}, + onTimeClick = {}, + ) + } +} + +@Preview +@Composable +private fun Preview_Due() { + ComponentPreview { + TrnTimeComponent( + extendedTrnTime = TrnTimeUi.Due( + dueOnDate = "Dec 21, 2021", + dueOnTime = "01:46 pm", + upcoming = true + ), + onDateClick = {}, + onTimeClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/CreateTrnController.kt b/transaction/src/main/java/com/ivy/transaction/create/CreateTrnController.kt new file mode 100644 index 0000000000..de59171fb5 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/CreateTrnController.kt @@ -0,0 +1,97 @@ +package com.ivy.transaction.create + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.focus.FocusRequester +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.util.KeyboardController +import com.ivy.transaction.create.action.CreateTrnStepsAct +import com.ivy.transaction.create.data.CreateTrnFlow +import com.ivy.transaction.create.data.CreateTrnStep +import javax.inject.Inject + +class CreateTrnController @Inject constructor( + private val createTrnStepsAct: CreateTrnStepsAct, +) { + val uiFlow: CreateTrnFlowUiState = CreateTrnFlowUiState.default() + private var createTrnFlow: CreateTrnFlow? = null + + private val titleStep = object : FlowStep { + override fun execute() { + uiFlow.titleFocus.requestFocus() + uiFlow.keyboardController.show() + } + } + private val amountStep = ModalStep(uiFlow.amountModal) + private val categoryStep = ModalStep(uiFlow.categoryPickerModal) + private val accountStep = ModalStep(uiFlow.accountPickerModal) + private val descriptionStep = ModalStep(uiFlow.descriptionModal) + private val dateStep = ModalStep(uiFlow.dateModal) + private val timeStep = ModalStep(uiFlow.timeModal) + + private fun flowStep(step: CreateTrnStep): FlowStep = when (step) { + CreateTrnStep.Title -> titleStep + CreateTrnStep.Amount -> amountStep + CreateTrnStep.Category -> categoryStep + CreateTrnStep.Account -> accountStep + CreateTrnStep.Description -> descriptionStep + CreateTrnStep.Date -> dateStep + CreateTrnStep.Time -> timeStep + } + + // region Public + suspend fun startFlow() { + val createTrnFlow = createTrnStepsAct(Unit).also { + this.createTrnFlow = it + } + flowStep(createTrnFlow.first).execute() + } + + fun nextStep(after: CreateTrnStep) { + createTrnFlow?.steps?.get(after)?.let(::flowStep)?.execute() + } + + fun hideKeyboard() { + uiFlow.keyboardController.hide() + } + // endregion + + + // region Helper classes + private interface FlowStep { + fun execute() + } + + class ModalStep(private val modal: IvyModal) : FlowStep { + override fun execute() { + modal.show() + } + } + // endregion +} + +// It would be better to be nested class +// But I'm not sure if it's Compose optimized that way +@Immutable +data class CreateTrnFlowUiState( + val keyboardController: KeyboardController, + val titleFocus: FocusRequester, + val amountModal: IvyModal, + val categoryPickerModal: IvyModal, + val accountPickerModal: IvyModal, + val descriptionModal: IvyModal, + val dateModal: IvyModal, + val timeModal: IvyModal, +) { + companion object { + fun default() = CreateTrnFlowUiState( + keyboardController = KeyboardController(), + titleFocus = FocusRequester(), + amountModal = IvyModal(), + categoryPickerModal = IvyModal(), + accountPickerModal = IvyModal(), + descriptionModal = IvyModal(), + dateModal = IvyModal(), + timeModal = IvyModal(), + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/action/CreateTrnStepsAct.kt b/transaction/src/main/java/com/ivy/transaction/create/action/CreateTrnStepsAct.kt new file mode 100644 index 0000000000..31c59b5001 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/action/CreateTrnStepsAct.kt @@ -0,0 +1,19 @@ +package com.ivy.transaction.create.action + +import com.ivy.core.domain.action.Action +import com.ivy.transaction.create.data.CreateTrnFlow +import com.ivy.transaction.create.data.CreateTrnStep +import javax.inject.Inject + +class CreateTrnStepsAct @Inject constructor( + +) : Action() { + override suspend fun Unit.willDo(): CreateTrnFlow = CreateTrnFlow( + first = CreateTrnStep.Amount, + steps = mapOf( + CreateTrnStep.Amount to CreateTrnStep.Category, + CreateTrnStep.Category to CreateTrnStep.Title, + ) + ) + +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/action/PreselectedAccountAct.kt b/transaction/src/main/java/com/ivy/transaction/create/action/PreselectedAccountAct.kt new file mode 100644 index 0000000000..a04f5570fe --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/action/PreselectedAccountAct.kt @@ -0,0 +1,41 @@ +package com.ivy.transaction.create.action + +import com.ivy.core.domain.action.Action +import com.ivy.core.domain.action.account.AccountByIdAct +import com.ivy.core.domain.action.account.AccountsAct +import com.ivy.core.persistence.datastore.IvyDataStore +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.transaction.create.action.PreselectedAccountAct.Input +import com.ivy.transaction.create.persistence.LastUsedAccountIdKey +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject + +class PreselectedAccountAct @Inject constructor( + private val dataStore: IvyDataStore, + private val accountByIdAct: AccountByIdAct, + private val mapAccountUiAct: MapAccountUiAct, + private val accountsAct: AccountsAct, + private val lastUsedAccountId: LastUsedAccountIdKey, +) : Action() { + + data class Input( + val preselectedAccountId: String?, + ) + + override suspend fun Input.willDo(): AccountUi? = + preselectedAccount() ?: lastUsedAccount() ?: firstAccount() + + private suspend fun Input.preselectedAccount(): AccountUi? = + preselectedAccountId?.let { accountByIdAct(it) } + ?.let { mapAccountUiAct(it) } + + private suspend fun lastUsedAccount(): AccountUi? = + dataStore.get(lastUsedAccountId.key).firstOrNull() + ?.let { accountByIdAct(it) } + ?.let { mapAccountUiAct(it) } + + private suspend fun firstAccount(): AccountUi? = + accountsAct(Unit).firstOrNull() + ?.let { mapAccountUiAct(it) } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/action/WriteLastUsedAccount.kt b/transaction/src/main/java/com/ivy/transaction/create/action/WriteLastUsedAccount.kt new file mode 100644 index 0000000000..c8a3e70298 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/action/WriteLastUsedAccount.kt @@ -0,0 +1,19 @@ +package com.ivy.transaction.create.action + +import com.ivy.core.domain.action.Action +import com.ivy.core.persistence.datastore.IvyDataStore +import com.ivy.transaction.create.persistence.LastUsedAccountIdKey +import javax.inject.Inject + +class WriteLastUsedAccount @Inject constructor( + private val dataStore: IvyDataStore, + private val lastUsedAccountId: LastUsedAccountIdKey, +) : Action() { + data class Input( + val accountId: String, + ) + + override suspend fun Input.willDo() { + dataStore.put(lastUsedAccountId.key, accountId) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/data/CreateTrnFlow.kt b/transaction/src/main/java/com/ivy/transaction/create/data/CreateTrnFlow.kt new file mode 100644 index 0000000000..6d30913c61 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/data/CreateTrnFlow.kt @@ -0,0 +1,6 @@ +package com.ivy.transaction.create.data + +data class CreateTrnFlow( + val first: CreateTrnStep, + val steps: Map +) \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/data/CreateTrnStep.kt b/transaction/src/main/java/com/ivy/transaction/create/data/CreateTrnStep.kt new file mode 100644 index 0000000000..091b7e08ab --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/data/CreateTrnStep.kt @@ -0,0 +1,11 @@ +package com.ivy.transaction.create.data + +enum class CreateTrnStep(val key: String) { + Title("title"), + Description("description"), + Amount("amount"), + Account("account"), + Category("category"), + Date("date"), + Time("time"), +} diff --git a/transaction/src/main/java/com/ivy/transaction/create/persistence/LastUsedAccountIdKey.kt b/transaction/src/main/java/com/ivy/transaction/create/persistence/LastUsedAccountIdKey.kt new file mode 100644 index 0000000000..97de5e432a --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/persistence/LastUsedAccountIdKey.kt @@ -0,0 +1,8 @@ +package com.ivy.transaction.create.persistence + +import androidx.datastore.preferences.core.stringPreferencesKey +import javax.inject.Inject + +class LastUsedAccountIdKey @Inject constructor() { + val key by lazy { stringPreferencesKey("last_used_account_id") } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferEvent.kt b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferEvent.kt new file mode 100644 index 0000000000..a55b388642 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferEvent.kt @@ -0,0 +1,25 @@ +package com.ivy.transaction.create.transfer + +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.data.Value +import com.ivy.data.transaction.TrnTime + +sealed interface NewTransferEvent { + object Initial : NewTransferEvent + object Add : NewTransferEvent + object Close : NewTransferEvent + + data class TransferAmountChange(val amount: Value) : NewTransferEvent + data class FromAmountChange(val amount: Value) : NewTransferEvent + data class ToAmountChange(val amount: Value) : NewTransferEvent + data class TitleChange(val title: String) : NewTransferEvent + data class DescriptionChange(val description: String?) : NewTransferEvent + data class FromAccountChange(val account: AccountUi) : NewTransferEvent + data class ToAccountChange(val account: AccountUi) : NewTransferEvent + data class CategoryChange(val category: CategoryUi?) : NewTransferEvent + data class TrnTimeChange(val time: TrnTime) : NewTransferEvent + data class FeePercent(val percent: Double) : NewTransferEvent + data class FeeChange(val value: Value?) : NewTransferEvent + data class RateChange(val newRate: Double) : NewTransferEvent +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferScreen.kt b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferScreen.kt new file mode 100644 index 0000000000..9c62b77da1 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferScreen.kt @@ -0,0 +1,337 @@ +package com.ivy.transaction.create.transfer + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ivy.core.domain.pure.dummy.dummyActual +import com.ivy.core.domain.pure.format.dummyCombinedValueUi +import com.ivy.core.ui.account.create.CreateAccountModal +import com.ivy.core.ui.category.pick.CategoryPickerModal +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.transaction.dummyTrnTimeActualUi +import com.ivy.core.ui.modals.RateModal +import com.ivy.design.l0_system.color.Blue2Dark +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.keyboardPadding +import com.ivy.design.util.keyboardShownState +import com.ivy.resources.R +import com.ivy.transaction.component.* +import com.ivy.transaction.create.CreateTrnFlowUiState +import com.ivy.transaction.data.TransferRateUi +import com.ivy.transaction.modal.* + +@Composable +fun BoxScope.NewTransferScreen() { + val viewModel: NewTransferViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + state.createFlow.keyboardController.wire() + + LaunchedEffect(Unit) { + viewModel.onEvent(NewTransferEvent.Initial) + } + + UI(state = state, onEvent = viewModel::onEvent) +} + +@Composable +private fun BoxScope.UI( + state: NewTransferState, + onEvent: (NewTransferEvent) -> Unit, +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + item(key = "toolbar") { + SpacerVer(height = 24.dp) + TrnScreenToolbar( + onClose = { + onEvent(NewTransferEvent.Close) + }, + actions = {}, + ) + } + item(key = "title") { + SpacerVer(height = 24.dp) + var titleFocused by remember { mutableStateOf(false) } + val keyboardShown by keyboardShownState() + TitleInput( + modifier = Modifier.onFocusChanged { + titleFocused = it.isFocused || it.hasFocus + }, + title = state.title, + focus = state.createFlow.titleFocus, + onTitleChange = { onEvent(NewTransferEvent.TitleChange(it)) }, + onCta = { onEvent(NewTransferEvent.Add) } + ) + TitleSuggestions( + focused = titleFocused && keyboardShown, + suggestions = state.titleSuggestions, + onSuggestionClick = { onEvent(NewTransferEvent.TitleChange(it)) } + ) + } + item(key = "category") { + SpacerVer(height = 12.dp) + CategoryComponent( + modifier = Modifier.padding(horizontal = 16.dp), + category = state.category + ) { + state.createFlow.categoryPickerModal.show() + } + } + item(key = "description") { + SpacerVer(height = 24.dp) + DescriptionComponent( + modifier = Modifier.padding(horizontal = 16.dp), + description = state.description + ) { + state.createFlow.descriptionModal.show() + } + } + item(key = "trn_time") { + SpacerVer(height = 12.dp) + TrnTimeComponent( + modifier = Modifier.padding(horizontal = 16.dp), + extendedTrnTime = state.timeUi, + onDateClick = { + state.createFlow.dateModal.show() + }, + onTimeClick = { + state.createFlow.timeModal.show() + } + ) + } + item(key = "fee_rate") { + SpacerVer(height = 12.dp) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + FeeComponent( + fee = state.fee.valueUi, + validFee = state.fee.value.amount > 0 + ) { + state.feeModal.show() + } + if (state.rate != null) { + SpacerHor(width = 12.dp) + TransferRateComponent( + modifier = Modifier.weight(1f), + rate = state.rate, + ) { + state.rateModal.show() + } + } + } + } + item(key = "last_item_spacer") { + val keyboardShown by keyboardShownState() + if (keyboardShown) { + SpacerVer(height = keyboardPadding()) + } + // To account for bottom sheet's height + SpacerVer(height = 520.dp) + } + } + + TransferBottomSheet( + accountFrom = state.accountFrom, + amountFromUi = state.amountFrom.valueUi, + amountFrom = state.amountFrom.value, + accountTo = state.accountTo, + amountToUi = state.amountTo.valueUi, + amountTo = state.amountTo.value, + ctaText = stringResource(R.string.add), + ctaIcon = R.drawable.ic_round_add_24, + onCtaClick = { + onEvent(NewTransferEvent.Add) + }, + onFromAccountChange = { + onEvent(NewTransferEvent.FromAccountChange(it)) + }, + onToAccountChange = { + onEvent(NewTransferEvent.ToAccountChange(it)) + }, + onFromAmountChange = { + onEvent(NewTransferEvent.FromAmountChange(it)) + }, + onToAmountChange = { + onEvent(NewTransferEvent.ToAmountChange(it)) + }, + ) + + Modals(state = state, onEvent = onEvent) +} + +@Composable +private fun BoxScope.Modals( + state: NewTransferState, + onEvent: (NewTransferEvent) -> Unit +) { + CategoryPickerModal( + modal = state.createFlow.categoryPickerModal, + selected = state.category, + trnType = null, + onPick = { + onEvent(NewTransferEvent.CategoryChange(it)) + } + ) + + DescriptionModal( + modal = state.createFlow.descriptionModal, + initialDescription = state.description, + onDescriptionChange = { + onEvent(NewTransferEvent.DescriptionChange(it)) + } + ) + + TrnDateModal( + modal = state.createFlow.dateModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(NewTransferEvent.TrnTimeChange(it)) + } + ) + TrnTimeModal( + modal = state.createFlow.timeModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(NewTransferEvent.TrnTimeChange(it)) + } + ) + + // Fee modal + FeeModal( + modal = state.feeModal, + fee = state.fee.value, + onRemoveFee = { + onEvent(NewTransferEvent.FeeChange(null)) + }, + onFeePercent = { + onEvent(NewTransferEvent.FeePercent(it)) + }, + onFeeChange = { + onEvent(NewTransferEvent.FeeChange(it)) + } + ) + + if (state.rate != null) { + RateModal( + modal = state.rateModal, + key = "transfer_rate", + rate = state.rate.rateValue, + fromCurrency = state.rate.fromCurrency, + toCurrency = state.rate.toCurrency, + onRateChange = { + onEvent(NewTransferEvent.RateChange(it)) + } + ) + } + + val createAccountModal = rememberIvyModal() + TransferAmountModal( + modal = state.createFlow.amountModal, + amount = state.amountFrom.value, + fromAccount = state.accountFrom, + toAccount = state.accountTo, + onAddAccount = { + createAccountModal.show() + }, + onAmountEnter = { + onEvent(NewTransferEvent.TransferAmountChange(it)) + }, + onFromAccountChange = { + onEvent(NewTransferEvent.FromAccountChange(it)) + }, + onToAccountChange = { + onEvent(NewTransferEvent.ToAccountChange(it)) + } + ) + + CreateAccountModal( + modal = createAccountModal, + level = 2, + ) +} + +// region Previews +@Preview +@Composable +private fun Preview() { + IvyPreview { + UI( + state = NewTransferState( + accountFrom = dummyAccountUi( + name = "Personal Bank", + color = Blue2Dark, + ), + amountFrom = dummyCombinedValueUi(), + accountTo = dummyAccountUi(name = "Cash"), + amountTo = dummyCombinedValueUi(), + category = dummyCategoryUi(), + description = null, + timeUi = dummyTrnTimeActualUi(), + time = dummyActual(), + title = null, + fee = dummyCombinedValueUi(), + rate = null, + + titleSuggestions = listOf("Title 1", "Title 2"), + createFlow = CreateTrnFlowUiState.default(), + feeModal = rememberIvyModal(), + rateModal = rememberIvyModal(), + ), + onEvent = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Filled() { + IvyPreview { + UI( + state = NewTransferState( + accountFrom = dummyAccountUi( + name = "Personal Bank", + color = Blue2Dark, + ), + amountFrom = dummyCombinedValueUi(amount = 400.0), + accountTo = dummyAccountUi(name = "Cash"), + amountTo = dummyCombinedValueUi(amount = 400.0), + category = dummyCategoryUi(), + description = "Need some cash", + timeUi = dummyTrnTimeActualUi(), + time = dummyActual(), + title = "ATM Withdrawal", + fee = dummyCombinedValueUi(amount = 2.0), + rate = TransferRateUi( + rateValue = 1.95, + rateValueFormatted = "1.95", + fromCurrency = "EUR", + toCurrency = "BGN", + ), + + titleSuggestions = listOf("Title 1", "Title 2"), + createFlow = CreateTrnFlowUiState.default(), + feeModal = rememberIvyModal(), + rateModal = rememberIvyModal(), + ), + onEvent = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferState.kt b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferState.kt new file mode 100644 index 0000000000..dc57a3d69d --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferState.kt @@ -0,0 +1,32 @@ +package com.ivy.transaction.create.transfer + +import androidx.compose.runtime.Immutable +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.transaction.TrnTime +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.transaction.create.CreateTrnFlowUiState +import com.ivy.transaction.data.TransferRateUi + +@Immutable +data class NewTransferState( + val accountFrom: AccountUi, + val accountTo: AccountUi, + val amountFrom: CombinedValueUi, + val amountTo: CombinedValueUi, + + val category: CategoryUi?, + val timeUi: TrnTimeUi, + val time: TrnTime, + val title: String?, + val description: String?, + val fee: CombinedValueUi, + val rate: TransferRateUi?, + + val titleSuggestions: List, + val createFlow: CreateTrnFlowUiState, + val feeModal: IvyModal, + val rateModal: IvyModal, +) \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferViewModel.kt b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferViewModel.kt new file mode 100644 index 0000000000..ea6e5e7311 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/transfer/NewTransferViewModel.kt @@ -0,0 +1,354 @@ +package com.ivy.transaction.create.transfer + +import com.ivy.common.time.provider.TimeProvider +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountByIdAct +import com.ivy.core.domain.action.account.AccountsAct +import com.ivy.core.domain.action.category.CategoryByIdAct +import com.ivy.core.domain.action.exchange.ExchangeAct +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyAct +import com.ivy.core.domain.action.transaction.transfer.ModifyTransfer +import com.ivy.core.domain.action.transaction.transfer.TransferData +import com.ivy.core.domain.action.transaction.transfer.WriteTransferAct +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.domain.pure.util.combine +import com.ivy.core.domain.pure.util.flattenLatest +import com.ivy.core.domain.pure.util.takeIfNotBlank +import com.ivy.core.ui.action.BaseCurrencyRepresentationFlow +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.core.ui.action.mapping.trn.MapTrnTimeUiAct +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.Value +import com.ivy.data.transaction.TrnTime +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.navigation.Navigator +import com.ivy.transaction.action.TitleSuggestionsFlow +import com.ivy.transaction.create.CreateTrnController +import com.ivy.transaction.create.action.CreateTrnStepsAct +import com.ivy.transaction.create.action.WriteLastUsedAccount +import com.ivy.transaction.data.TransferRateUi +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import java.text.DecimalFormat +import javax.inject.Inject + +@HiltViewModel +class NewTransferViewModel @Inject constructor( + timeProvider: TimeProvider, + private val titleSuggestionsFlow: TitleSuggestionsFlow, + private val createTrnStepsAct: CreateTrnStepsAct, + private val mapTrnTimeUiAct: MapTrnTimeUiAct, + private val navigator: Navigator, + private val accountByIdAct: AccountByIdAct, + private val categoryByIdAct: CategoryByIdAct, + private val mapCategoryUiAct: MapCategoryUiAct, + private val baseCurrencyAct: BaseCurrencyAct, + private val writeLastUsedAccount: WriteLastUsedAccount, + private val baseCurrencyRepresentationFlow: BaseCurrencyRepresentationFlow, + private val accountsAct: AccountsAct, + private val mapAccountUiAct: MapAccountUiAct, + private val writeTransferAct: WriteTransferAct, + private val exchangeAct: ExchangeAct, + private val createTrnController: CreateTrnController, +) : SimpleFlowViewModel() { + private val feeModal = IvyModal() + private val rateModal = IvyModal() + + override val initialUi = NewTransferState( + accountFrom = dummyAccountUi(), + accountTo = dummyAccountUi(), + amountFrom = CombinedValueUi.initial(), + amountTo = CombinedValueUi.initial(), + category = null, + timeUi = TrnTimeUi.Actual("", ""), + time = TrnTime.Actual(timeProvider.timeNow()), + title = null, + description = null, + fee = CombinedValueUi.initial(), + rate = null, + + titleSuggestions = emptyList(), + createFlow = createTrnController.uiFlow, + feeModal = feeModal, + rateModal = rateModal, + ) + + // region State + private val amountFrom = MutableStateFlow(initialUi.amountFrom) + private val amountTo = MutableStateFlow(initialUi.amountTo) + private val accountFrom = MutableStateFlow(initialUi.accountFrom) + private val accountTo = MutableStateFlow(initialUi.accountTo) + private val category = MutableStateFlow(initialUi.category) + private val time = MutableStateFlow(TrnTime.Actual(timeProvider.timeNow())) + private val timeUi = MutableStateFlow(initialUi.timeUi) + private val title = MutableStateFlow(initialUi.title) + private val description = MutableStateFlow(initialUi.description) + private val fee = MutableStateFlow(initialUi.fee) + // endregion + + + override val uiFlow = combine( + amountFrom, amountTo, + accountFrom, accountTo, category, time, timeUi, + title, description, fee, + ) + { amountFrom, amountTo, + accountFrom, accountTo, category, time, timeUi, + title, description, fee -> + titleSuggestionsFlow( + TitleSuggestionsFlow.Input( + title = title, + categoryUi = category, + transfer = true, + ) + ).map { titleSuggestions -> + NewTransferState( + amountFrom = amountFrom, + amountTo = amountTo, + accountFrom = accountFrom, + accountTo = accountTo, + category = category, + time = time, + timeUi = timeUi, + title = title, + description = description, + fee = fee, + rate = if (amountFrom.value.currency != amountTo.value.currency && + amountFrom.value.amount > 0.0 && amountTo.value.amount > 0.0 + ) { + // e.g. 1 EUR to 1.96 BGN + // => EUR-BGN = 1.96 / 1 = 1.96 + val rateValue = amountTo.value.amount / amountFrom.value.amount + TransferRateUi( + rateValueFormatted = DecimalFormat( + "###,###,##0.${"#".repeat(6)}" + ).format(rateValue), + rateValue = rateValue, + fromCurrency = amountFrom.value.currency, + toCurrency = amountTo.value.currency, + ) + } else null, + + titleSuggestions = titleSuggestions, + createFlow = createTrnController.uiFlow, + feeModal = feeModal, + rateModal = rateModal, + ) + } + }.flattenLatest() + + + // region Event Handling + override suspend fun handleEvent(event: NewTransferEvent) = when (event) { + NewTransferEvent.Initial -> handleInitial() + NewTransferEvent.Close -> handleClose() + NewTransferEvent.Add -> handleAdd() + is NewTransferEvent.TransferAmountChange -> handleTransferAmountChange(event) + is NewTransferEvent.ToAmountChange -> handleToAmountChange(event) + is NewTransferEvent.FromAmountChange -> handleFromAmountChange(event) + is NewTransferEvent.FromAccountChange -> handleFromAccountChange(event) + is NewTransferEvent.ToAccountChange -> handleToAccountChange(event) + is NewTransferEvent.FeeChange -> handleFeeChange(event) + is NewTransferEvent.FeePercent -> handleFeePercent(event) + is NewTransferEvent.TitleChange -> handleTitleChange(event) + is NewTransferEvent.DescriptionChange -> handleDescriptionChange(event) + is NewTransferEvent.CategoryChange -> handleCategoryChange(event) + is NewTransferEvent.TrnTimeChange -> handleTimeChange(event) + is NewTransferEvent.RateChange -> handleRateChange(event) + } + + private suspend fun handleInitial() { + createTrnController.startFlow() + + val accounts = accountsAct(Unit) + if (accounts.size < 2) { + // cannot do transfers with less than 2 accounts + closeScreen() + return + } + val fromAcc = accounts.first() + val toAcc = accounts[1] // 2nd + + accountFrom.value = mapAccountUiAct(fromAcc) + accountTo.value = mapAccountUiAct(toAcc) + + amountFrom.value = CombinedValueUi( + amount = 0.0, + currency = fromAcc.currency, + shortenFiat = false, + ) + fee.value = CombinedValueUi( + amount = 0.0, + currency = fromAcc.currency, + shortenFiat = false, + ) + amountTo.value = CombinedValueUi( + amount = 0.0, + currency = toAcc.currency, + shortenFiat = false, + ) + + timeUi.value = mapTrnTimeUiAct(time.value) + } + + private suspend fun handleAdd() { + val accountFrom = accountByIdAct(accountFrom.value.id) ?: return + val accountTo = accountByIdAct(accountTo.value.id) ?: return + val category = category.value?.let { categoryByIdAct(it.id) } + + val data = TransferData( + accountFrom = accountFrom, + accountTo = accountTo, + amountFrom = amountFrom.value.value, + amountTo = amountTo.value.value, + category = category, + time = time.value, + title = title.value, + description = description.value, + fee = fee.value.value.takeIf { it.amount > 0.0 }, + ) + + writeTransferAct(ModifyTransfer.add(data)) + + closeScreen() + } + + private fun handleClose() { + closeScreen() + } + + private fun closeScreen() { + createTrnController.hideKeyboard() + navigator.back() + } + + // region Handle value changes + private suspend fun handleTransferAmountChange(event: NewTransferEvent.TransferAmountChange) { + // Called initially when the transfer modal is shown + updateFromAmount(event.amount) + } + + private suspend fun handleFromAmountChange(event: NewTransferEvent.FromAmountChange) { + updateFromAmount(event.amount) + } + + private suspend fun updateFromAmount( + newFromAmount: Value + ) { + val toAccount = accountByIdAct(accountTo.value.id) ?: return + + amountFrom.value = CombinedValueUi( + value = newFromAmount, + shortenFiat = false, + ) + + val rate = uiState.value.rate + if (rate != null && rate.rateValue > 0) { + // Custom exchange rate set by the user, use it + amountTo.value = CombinedValueUi( + amount = newFromAmount.amount * rate.rateValue, + currency = toAccount.currency, + shortenFiat = false, + ) + } else { + // No rate, exchange by latest rate + amountTo.value = CombinedValueUi( + value = exchangeAct( + ExchangeAct.Input( + value = newFromAmount, + outputCurrency = toAccount.currency + ) + ), + shortenFiat = false, + ) + } + } + + private fun handleToAmountChange(event: NewTransferEvent.ToAmountChange) { + amountTo.value = CombinedValueUi( + value = event.amount, + shortenFiat = false, + ) + } + + private suspend fun handleFromAccountChange(event: NewTransferEvent.FromAccountChange) { + accountFrom.value = event.account + + accountByIdAct(event.account.id)?.let { + amountFrom.value = CombinedValueUi( + amount = amountFrom.value.value.amount, + currency = it.currency, + shortenFiat = false, + ) + fee.value = CombinedValueUi( + amount = fee.value.value.amount, + currency = it.currency, + shortenFiat = false, + ) + } + } + + private suspend fun handleToAccountChange(event: NewTransferEvent.ToAccountChange) { + accountTo.value = event.account + + accountByIdAct(event.account.id)?.let { + amountTo.value = CombinedValueUi( + amount = amountTo.value.value.amount, + currency = it.currency, + shortenFiat = false, + ) + } + } + + private fun handleFeeChange(event: NewTransferEvent.FeeChange) { + fee.value = if (event.value != null) CombinedValueUi( + value = event.value, + shortenFiat = false, + ) else { + // no fee (0 fee) + CombinedValueUi( + amount = 0.0, + currency = fee.value.value.currency, + shortenFiat = false, + ) + } + } + + private fun handleFeePercent(event: NewTransferEvent.FeePercent) { + fee.value = CombinedValueUi( + amount = amountFrom.value.value.amount * event.percent, + currency = fee.value.value.currency, + shortenFiat = false, + ) + } + + private fun handleRateChange(event: NewTransferEvent.RateChange) { + amountTo.value = CombinedValueUi( + amount = amountFrom.value.value.amount * event.newRate, + currency = amountTo.value.value.currency, + shortenFiat = false, + ) + } + + private fun handleTitleChange(event: NewTransferEvent.TitleChange) { + title.value = event.title.takeIfNotBlank() + } + + private fun handleDescriptionChange(event: NewTransferEvent.DescriptionChange) { + description.value = event.description.takeIfNotBlank() + } + + private fun handleCategoryChange(event: NewTransferEvent.CategoryChange) { + category.value = event.category + } + + private suspend fun handleTimeChange(event: NewTransferEvent.TrnTimeChange) { + time.value = event.time + timeUi.value = mapTrnTimeUiAct(event.time) + } + // endregion + // endregion +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/trn/NewTransactionScreen.kt b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTransactionScreen.kt new file mode 100644 index 0000000000..62ca9ab74a --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTransactionScreen.kt @@ -0,0 +1,287 @@ +package com.ivy.transaction.create.trn + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ivy.core.domain.pure.format.dummyCombinedValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.category.pick.CategoryPickerModal +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.transaction.dummyTrnTimeActualUi +import com.ivy.core.ui.data.transaction.dummyTrnTimeDueUi +import com.ivy.core.ui.transaction.feeling +import com.ivy.core.ui.transaction.humanText +import com.ivy.core.ui.transaction.icon +import com.ivy.data.transaction.TransactionType +import com.ivy.data.transaction.dummyTrnTimeActual +import com.ivy.data.transaction.dummyTrnTimeDue +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.keyboardPadding +import com.ivy.design.util.keyboardShownState +import com.ivy.navigation.destinations.transaction.NewTransaction +import com.ivy.resources.R +import com.ivy.transaction.component.* +import com.ivy.transaction.create.CreateTrnFlowUiState +import com.ivy.transaction.modal.DescriptionModal +import com.ivy.transaction.modal.TrnDateModal +import com.ivy.transaction.modal.TrnTimeModal +import com.ivy.transaction.modal.TrnTypeModal + +@Composable +fun BoxScope.NewTransactionScreen(arg: NewTransaction.Arg) { + val viewModel: NewTransactionViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + state.createFlow.keyboardController.wire() + + LaunchedEffect(Unit) { + viewModel.onEvent(NewTrnEvent.Initial(arg)) + } + + UI( + state = state, + onEvent = viewModel::onEvent, + ) +} + +@Composable +private fun BoxScope.UI( + state: NewTrnState, + onEvent: (NewTrnEvent) -> Unit, +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + item(key = "toolbar") { + SpacerVer(height = 24.dp) + NewTrnScreenToolbar( + onClose = { + onEvent(NewTrnEvent.Close) + }, + trnType = state.trnType, + onChangeTrnType = { + state.trnTypeModal.show() + } + ) + } + item(key = "title") { + SpacerVer(height = 24.dp) + var titleFocused by remember { mutableStateOf(false) } + val keyboardShown by keyboardShownState() + TitleInput( + modifier = Modifier.onFocusChanged { + titleFocused = it.isFocused || it.hasFocus + }, + title = state.title, + focus = state.createFlow.titleFocus, + onTitleChange = { onEvent(NewTrnEvent.TitleChange(it)) }, + onCta = { onEvent(NewTrnEvent.Add) } + ) + TitleSuggestions( + focused = titleFocused && keyboardShown, + suggestions = state.titleSuggestions, + onSuggestionClick = { onEvent(NewTrnEvent.TitleChange(it)) } + ) + } + item(key = "category") { + SpacerVer(height = 12.dp) + CategoryComponent( + modifier = Modifier.padding(horizontal = 16.dp), + category = state.category + ) { + state.createFlow.categoryPickerModal.show() + } + } + item(key = "description") { + SpacerVer(height = 24.dp) + DescriptionComponent( + modifier = Modifier.padding(horizontal = 16.dp), + description = state.description + ) { + state.createFlow.descriptionModal.show() + } + } + item(key = "trn_time") { + SpacerVer(height = 12.dp) + TrnTimeComponent( + modifier = Modifier.padding(horizontal = 16.dp), + extendedTrnTime = state.timeUi, + onDateClick = { + state.createFlow.dateModal.show() + }, + onTimeClick = { + state.createFlow.timeModal.show() + } + ) + } + item(key = "last_item_spacer") { + val keyboardShown by keyboardShownState() + if (keyboardShown) { + SpacerVer(height = keyboardPadding()) + } + // To account for "Amount Account sheet" height + SpacerVer(height = 480.dp) + } + } + + AmountAccountSheet( + amountUi = state.amount.valueUi, + amount = state.amount.value, + amountBaseCurrency = state.amountBaseCurrency, + account = state.account, + ctaText = stringResource(R.string.add), + ctaIcon = R.drawable.ic_round_add_24, + accountPickerModal = state.createFlow.accountPickerModal, + amountModal = state.createFlow.amountModal, + onAccountChange = { + onEvent(NewTrnEvent.AccountChange(it)) + }, + onAmountEnter = { + onEvent(NewTrnEvent.AmountChange(it)) + }, + onCtaClick = { + onEvent(NewTrnEvent.Add) + } + ) + + Modals(state = state, onEvent = onEvent) +} + +@Composable +private fun BoxScope.Modals( + state: NewTrnState, + onEvent: (NewTrnEvent) -> Unit +) { + CategoryPickerModal( + modal = state.createFlow.categoryPickerModal, + selected = state.category, + trnType = state.trnType, + onPick = { + onEvent(NewTrnEvent.CategoryChange(it)) + } + ) + + DescriptionModal( + modal = state.createFlow.descriptionModal, + initialDescription = state.description, + onDescriptionChange = { + onEvent(NewTrnEvent.DescriptionChange(it)) + } + ) + + TrnTypeModal( + modal = state.trnTypeModal, + trnType = state.trnType, + onTransactionTypeChange = { + onEvent(NewTrnEvent.TrnTypeChange(it)) + } + ) + + TrnDateModal( + modal = state.createFlow.dateModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(NewTrnEvent.TrnTimeChange(it)) + } + ) + TrnTimeModal( + modal = state.createFlow.timeModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(NewTrnEvent.TrnTimeChange(it)) + } + ) +} + +@Composable +private fun NewTrnScreenToolbar( + onClose: () -> Unit, + trnType: TransactionType, + onChangeTrnType: () -> Unit, +) { + TrnScreenToolbar( + onClose = onClose, + actions = { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = trnType.feeling(), + text = trnType.humanText(), + icon = trnType.icon(), + onClick = onChangeTrnType, + ) + } + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview_Empty() { + IvyPreview { + UI( + state = NewTrnState( + trnType = TransactionType.Income, + category = null, + description = null, + amount = dummyCombinedValueUi(), + amountBaseCurrency = null, + account = dummyAccountUi(), + title = null, + + titleSuggestions = emptyList(), + + timeUi = dummyTrnTimeActualUi(), + time = dummyTrnTimeActual(), + trnTypeModal = rememberIvyModal(), + createFlow = CreateTrnFlowUiState.default(), + ), + onEvent = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Filled() { + IvyPreview { + UI( + state = NewTrnState( + trnType = TransactionType.Expense, + title = "Tabu Shisha", + category = dummyCategoryUi(), + description = "Lorem ipsum blablablabla okay good test\n1\n2\n", + amount = dummyCombinedValueUi(amount = 23.99), + amountBaseCurrency = dummyValueUi(amount = "48.23", currency = "BGN"), + account = dummyAccountUi(), + + titleSuggestions = emptyList(), + + timeUi = dummyTrnTimeDueUi(), + time = dummyTrnTimeDue(), + trnTypeModal = rememberIvyModal(), + createFlow = CreateTrnFlowUiState.default(), + ), + onEvent = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/trn/NewTransactionViewModel.kt b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTransactionViewModel.kt new file mode 100644 index 0000000000..3b366eee13 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTransactionViewModel.kt @@ -0,0 +1,274 @@ +package com.ivy.transaction.create.trn + +import com.ivy.common.isNotNullOrBlank +import com.ivy.common.time.provider.TimeProvider +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountByIdAct +import com.ivy.core.domain.action.category.CategoryByIdAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyAct +import com.ivy.core.domain.action.transaction.WriteTrnsAct +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.domain.pure.util.flattenLatest +import com.ivy.core.ui.action.BaseCurrencyRepresentationFlow +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.action.mapping.trn.MapTrnTimeUiAct +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.SyncState +import com.ivy.data.transaction.* +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.navigation.Navigator +import com.ivy.transaction.action.TitleSuggestionsFlow +import com.ivy.transaction.create.CreateTrnController +import com.ivy.transaction.create.action.CreateTrnStepsAct +import com.ivy.transaction.create.action.PreselectedAccountAct +import com.ivy.transaction.create.action.WriteLastUsedAccount +import com.ivy.transaction.create.data.CreateTrnStep +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import java.util.* +import javax.inject.Inject + +@HiltViewModel +class NewTransactionViewModel @Inject constructor( + timeProvider: TimeProvider, + private val createTrnStepsAct: CreateTrnStepsAct, + private val mapTrnTimeUiAct: MapTrnTimeUiAct, + private val navigator: Navigator, + private val writeTrnsAct: WriteTrnsAct, + private val accountByIdAct: AccountByIdAct, + private val categoryByIdAct: CategoryByIdAct, + private val mapCategoryUiAct: MapCategoryUiAct, + private val preselectedAccountAct: PreselectedAccountAct, + private val baseCurrencyAct: BaseCurrencyAct, + private val writeLastUsedAccount: WriteLastUsedAccount, + private val baseCurrencyRepresentationFlow: BaseCurrencyRepresentationFlow, + private val titleSuggestionsFlow: TitleSuggestionsFlow, + private val createTrnController: CreateTrnController, +) : SimpleFlowViewModel() { + + private val trnTypeModal = IvyModal() + + override val initialUi = NewTrnState( + trnType = TransactionType.Expense, + amount = CombinedValueUi.initial(), + amountBaseCurrency = null, + account = dummyAccountUi(), + category = null, + timeUi = TrnTimeUi.Actual("", ""), + time = TrnTime.Actual(timeProvider.timeNow()), + title = null, + description = null, + + titleSuggestions = emptyList(), + + createFlow = createTrnController.uiFlow, + trnTypeModal = trnTypeModal, + ) + + // region State + private val trnType = MutableStateFlow(initialUi.trnType) + private val amount = MutableStateFlow(initialUi.amount) + private val account = MutableStateFlow(initialUi.account) + private val category = MutableStateFlow(initialUi.category) + private val time = MutableStateFlow(TrnTime.Actual(timeProvider.timeNow())) + private val timeUi = MutableStateFlow(initialUi.timeUi) + private val title = MutableStateFlow(initialUi.title) + private val description = MutableStateFlow(initialUi.description) + // endregion + + override val uiFlow: Flow = combine( + trnType, amountFlow(), accountCategoryFlow(), textsFlow(), timeFlow(), + ) { trnType, (amount, amountBaseCurrency), (account, category), + (title, description, titleSuggestions), (time, timeUi) -> + NewTrnState( + trnType = trnType, + amount = amount, + amountBaseCurrency = amountBaseCurrency, + account = account, + category = category, + timeUi = timeUi, + time = time, + title = title, + description = description, + + titleSuggestions = titleSuggestions, + createFlow = createTrnController.uiFlow, + trnTypeModal = trnTypeModal, + ) + } + + private fun amountFlow() = amount.map { amount -> + baseCurrencyRepresentationFlow(amount.value).map { amountBaseCurrency -> + amount to amountBaseCurrency + } + }.flattenLatest() + + private fun textsFlow() = combine( + title, description, category, + ) { title, description, category -> + titleSuggestionsFlow( + TitleSuggestionsFlow.Input( + title = title, + categoryUi = category, + transfer = false, + ) + ).map { titleSuggestions -> + Triple(title, description, titleSuggestions) + } + }.flattenLatest() + + private fun accountCategoryFlow() = combine( + account, category + ) { account, category -> + account to category + } + + private fun timeFlow() = combine( + time, timeUi + ) { time, timeUi -> + time to timeUi + } + + // region Event Handling + override suspend fun handleEvent(event: NewTrnEvent) = when (event) { + is NewTrnEvent.Initial -> handleInitial(event) + is NewTrnEvent.AccountChange -> handleAccountChange(event) + NewTrnEvent.Add -> handleAdd() + is NewTrnEvent.AmountChange -> handleAmountChange(event) + is NewTrnEvent.CategoryChange -> handleCategoryChange(event) + NewTrnEvent.Close -> handleClose() + is NewTrnEvent.TitleChange -> handleTitleChange(event) + is NewTrnEvent.DescriptionChange -> handleDescriptionChange(event) + is NewTrnEvent.TrnTimeChange -> handleTrnTimeChange(event) + is NewTrnEvent.TrnTypeChange -> handleTrnTypeChange(event) + } + + private suspend fun handleInitial(event: NewTrnEvent.Initial) { + createTrnController.startFlow() + + val arg = event.arg + trnType.value = arg.trnType + category.value = arg.categoryId?.let { + categoryByIdAct(it) + }?.let { + mapCategoryUiAct.invoke(it) + } + preselectedAccountAct( + PreselectedAccountAct.Input( + preselectedAccountId = arg.accountId + ) + )?.let { + account.value = it + } + timeUi.value = mapTrnTimeUiAct(time.value) + amount.value = CombinedValueUi( + amount = 0.0, + currency = baseCurrencyAct(Unit), + shortenFiat = false, + ) + } + + private suspend fun handleAdd() { + val account = accountByIdAct(account.value.id) ?: return + val category = category.value?.id?.let { categoryByIdAct(it) } + + val transaction = Transaction( + id = UUID.randomUUID(), + account = account, + category = category, + type = trnType.value, + value = amount.value.value, + time = time.value, + title = title.value, + description = description.value, + state = TrnState.Default, + purpose = null, + sync = SyncState.Syncing, + tags = emptyList(), + attachments = emptyList(), + metadata = TrnMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null, + ) + ) + + writeTrnsAct(Modify.save(transaction)) + closeScreen() + } + + + private fun handleClose() { + closeScreen() + } + + private fun closeScreen() { + createTrnController.hideKeyboard() + navigator.back() + } + + // region Handle Value changes + private fun handleAmountChange(event: NewTrnEvent.AmountChange) { + amount.value = CombinedValueUi( + value = event.amount, + shortenFiat = false + ) + + createTrnController.nextStep(after = CreateTrnStep.Amount) + } + + private suspend fun handleAccountChange(event: NewTrnEvent.AccountChange) { + account.value = event.account + writeLastUsedAccount(WriteLastUsedAccount.Input(event.account.id)) + changeAmountToAccountCurrency(event.account) + + createTrnController.nextStep(after = CreateTrnStep.Account) + } + + private suspend fun changeAmountToAccountCurrency( + account: AccountUi + ) { + accountByIdAct(account.id)?.let { + val accountCurrency = it.currency + amount.value = CombinedValueUi( + value = amount.value.value.copy(currency = accountCurrency), + shortenFiat = false + ) + } + } + + private fun handleCategoryChange(event: NewTrnEvent.CategoryChange) { + category.value = event.category + + createTrnController.nextStep(after = CreateTrnStep.Category) + } + + private fun handleTitleChange(event: NewTrnEvent.TitleChange) { + title.value = event.title.takeIf { it.isNotBlank() } + } + + private fun handleDescriptionChange(event: NewTrnEvent.DescriptionChange) { + description.value = event.description.takeIf { it.isNotNullOrBlank() } + + createTrnController.nextStep(after = CreateTrnStep.Description) + } + + private suspend fun handleTrnTimeChange(event: NewTrnEvent.TrnTimeChange) { + time.value = event.time + timeUi.value = mapTrnTimeUiAct(event.time) + + createTrnController.nextStep(after = CreateTrnStep.Date) + } + + private fun handleTrnTypeChange(event: NewTrnEvent.TrnTypeChange) { + trnType.value = event.trnType + } + // endregion + // endregion +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/trn/NewTrnEvent.kt b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTrnEvent.kt new file mode 100644 index 0000000000..05f119f4df --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTrnEvent.kt @@ -0,0 +1,22 @@ +package com.ivy.transaction.create.trn + +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.data.Value +import com.ivy.data.transaction.TransactionType +import com.ivy.data.transaction.TrnTime +import com.ivy.navigation.destinations.transaction.NewTransaction + +sealed interface NewTrnEvent { + data class Initial(val arg: NewTransaction.Arg) : NewTrnEvent + object Add : NewTrnEvent + object Close : NewTrnEvent + + data class AmountChange(val amount: Value) : NewTrnEvent + data class TitleChange(val title: String) : NewTrnEvent + data class DescriptionChange(val description: String?) : NewTrnEvent + data class AccountChange(val account: AccountUi) : NewTrnEvent + data class CategoryChange(val category: CategoryUi?) : NewTrnEvent + data class TrnTypeChange(val trnType: TransactionType) : NewTrnEvent + data class TrnTimeChange(val time: TrnTime) : NewTrnEvent +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/create/trn/NewTrnState.kt b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTrnState.kt new file mode 100644 index 0000000000..309bc9d694 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/create/trn/NewTrnState.kt @@ -0,0 +1,32 @@ +package com.ivy.transaction.create.trn + +import androidx.compose.runtime.Immutable +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.transaction.TransactionType +import com.ivy.data.transaction.TrnTime +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.transaction.create.CreateTrnFlowUiState + +@Immutable +data class NewTrnState( + val trnType: TransactionType, + val amount: CombinedValueUi, + val amountBaseCurrency: ValueUi?, + val account: AccountUi, + val category: CategoryUi?, + val timeUi: TrnTimeUi, + val time: TrnTime, + val title: String?, + val description: String?, + + val titleSuggestions: List, + + // region Create flow + val createFlow: CreateTrnFlowUiState, + val trnTypeModal: IvyModal, + // endregion +) \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/data/TransferRateUi.kt b/transaction/src/main/java/com/ivy/transaction/data/TransferRateUi.kt new file mode 100644 index 0000000000..041c121a85 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/data/TransferRateUi.kt @@ -0,0 +1,12 @@ +package com.ivy.transaction.data + +import androidx.compose.runtime.Immutable +import com.ivy.data.CurrencyCode + +@Immutable +data class TransferRateUi( + val rateValue: Double, + val rateValueFormatted: String, + val fromCurrency: CurrencyCode, + val toCurrency: CurrencyCode, +) \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferEvent.kt b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferEvent.kt new file mode 100644 index 0000000000..1565f73162 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferEvent.kt @@ -0,0 +1,25 @@ +package com.ivy.transaction.edit.transfer + +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.data.Value +import com.ivy.data.transaction.TrnTime + +sealed interface EditTransferEvent { + data class Initial(val batchId: String) : EditTransferEvent + object Save : EditTransferEvent + object Close : EditTransferEvent + object Delete : EditTransferEvent + + data class FromAmountChange(val amount: Value) : EditTransferEvent + data class ToAmountChange(val amount: Value) : EditTransferEvent + data class TitleChange(val title: String) : EditTransferEvent + data class DescriptionChange(val description: String?) : EditTransferEvent + data class FromAccountChange(val account: AccountUi) : EditTransferEvent + data class ToAccountChange(val account: AccountUi) : EditTransferEvent + data class CategoryChange(val category: CategoryUi?) : EditTransferEvent + data class TrnTimeChange(val time: TrnTime) : EditTransferEvent + data class FeeChange(val value: Value?) : EditTransferEvent + data class FeePercent(val percent: Double) : EditTransferEvent + data class RateChange(val newRate: Double) : EditTransferEvent +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferScreen.kt b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferScreen.kt new file mode 100644 index 0000000000..e1bcd7d159 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferScreen.kt @@ -0,0 +1,350 @@ +package com.ivy.transaction.edit.transfer + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ivy.core.domain.pure.dummy.dummyActual +import com.ivy.core.domain.pure.format.dummyCombinedValueUi +import com.ivy.core.ui.category.pick.CategoryPickerModal +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.transaction.dummyTrnTimeActualUi +import com.ivy.core.ui.modals.RateModal +import com.ivy.design.l0_system.color.Blue2Dark +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.button.DeleteButton +import com.ivy.design.l3_ivyComponents.modal.DeleteConfirmationModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.KeyboardController +import com.ivy.design.util.keyboardPadding +import com.ivy.design.util.keyboardShownState +import com.ivy.resources.R +import com.ivy.transaction.component.* +import com.ivy.transaction.data.TransferRateUi +import com.ivy.transaction.modal.DescriptionModal +import com.ivy.transaction.modal.FeeModal +import com.ivy.transaction.modal.TrnDateModal +import com.ivy.transaction.modal.TrnTimeModal + +@Composable +fun BoxScope.EditTransferScreen( + batchId: String, +) { + val viewModel: EditTransferViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.onEvent(EditTransferEvent.Initial(batchId = batchId)) + } + + UI(state = state, onEvent = viewModel::onEvent) +} + +@Composable +private fun BoxScope.UI( + state: EditTransferState, + onEvent: (EditTransferEvent) -> Unit, +) { + val dateModal = rememberIvyModal() + val timeModal = rememberIvyModal() + val categoryPickerModal = rememberIvyModal() + val descriptionModal = rememberIvyModal() + val deleteConfirmationModal = rememberIvyModal() + val feeModal = rememberIvyModal() + val rateModal = rememberIvyModal() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + item(key = "toolbar") { + SpacerVer(height = 24.dp) + TrnScreenToolbar( + onClose = { + onEvent(EditTransferEvent.Close) + }, + actions = {}, + ) + } + item(key = "title") { + SpacerVer(height = 24.dp) + val titleFocus = remember { FocusRequester() } + var titleFocused by remember { mutableStateOf(false) } + val keyboardShown by keyboardShownState() + TitleInput( + modifier = Modifier.onFocusChanged { + titleFocused = it.isFocused || it.hasFocus + }, + title = state.title, + focus = titleFocus, + onTitleChange = { onEvent(EditTransferEvent.TitleChange(it)) }, + onCta = { onEvent(EditTransferEvent.Save) } + ) + TitleSuggestions( + focused = titleFocused && keyboardShown, + suggestions = state.titleSuggestions, + onSuggestionClick = { onEvent(EditTransferEvent.TitleChange(it)) } + ) + } + item(key = "category") { + SpacerVer(height = 12.dp) + CategoryComponent( + modifier = Modifier.padding(horizontal = 16.dp), + category = state.category + ) { + categoryPickerModal.show() + } + } + item(key = "description") { + SpacerVer(height = 24.dp) + DescriptionComponent( + modifier = Modifier.padding(horizontal = 16.dp), + description = state.description + ) { + descriptionModal.show() + } + } + item(key = "trn_time") { + SpacerVer(height = 12.dp) + TrnTimeComponent( + modifier = Modifier.padding(horizontal = 16.dp), + extendedTrnTime = state.timeUi, + onTimeClick = { + timeModal.show() + }, + onDateClick = { + dateModal.show() + } + ) + } + item(key = "fee_rate") { + SpacerVer(height = 12.dp) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + FeeComponent( + fee = state.fee.valueUi, + validFee = state.fee.value.amount > 0 + ) { + feeModal.show() + } + if (state.rate != null) { + SpacerHor(width = 12.dp) + TransferRateComponent( + modifier = Modifier.weight(1f), + rate = state.rate, + ) { + rateModal.show() + } + } + } + } + item(key = "last_item_spacer") { + val keyboardShown by keyboardShownState() + if (keyboardShown) { + SpacerVer(height = keyboardPadding()) + } + // To account for bottom sheet's height + SpacerVer(height = 520.dp) + } + } + + TransferBottomSheet( + accountFrom = state.accountFrom, + amountFromUi = state.amountFrom.valueUi, + amountFrom = state.amountFrom.value, + accountTo = state.accountTo, + amountToUi = state.amountTo.valueUi, + amountTo = state.amountTo.value, + ctaText = stringResource(R.string.save), + ctaIcon = R.drawable.round_done_24, + secondaryActions = { + DeleteButton { + deleteConfirmationModal.show() + } + SpacerHor(width = 12.dp) + }, + onCtaClick = { + onEvent(EditTransferEvent.Save) + }, + onFromAccountChange = { + onEvent(EditTransferEvent.FromAccountChange(it)) + }, + onToAccountChange = { + onEvent(EditTransferEvent.ToAccountChange(it)) + }, + onFromAmountChange = { + onEvent(EditTransferEvent.FromAmountChange(it)) + }, + onToAmountChange = { + onEvent(EditTransferEvent.ToAmountChange(it)) + }, + ) + + Modals( + state = state, + dateModal = dateModal, + timeModal = timeModal, + descriptionModal = descriptionModal, + categoryPickerModal = categoryPickerModal, + deleteConfirmationModal = deleteConfirmationModal, + feeModal = feeModal, + rateModal = rateModal, + onEvent = onEvent + ) +} + +@Composable +private fun BoxScope.Modals( + state: EditTransferState, + dateModal: IvyModal, + timeModal: IvyModal, + descriptionModal: IvyModal, + categoryPickerModal: IvyModal, + deleteConfirmationModal: IvyModal, + feeModal: IvyModal, + rateModal: IvyModal, + onEvent: (EditTransferEvent) -> Unit +) { + CategoryPickerModal( + modal = categoryPickerModal, + selected = state.category, + trnType = null, + onPick = { + onEvent(EditTransferEvent.CategoryChange(it)) + } + ) + + DescriptionModal( + modal = descriptionModal, + initialDescription = state.description, + onDescriptionChange = { + onEvent(EditTransferEvent.DescriptionChange(it)) + } + ) + + TrnDateModal( + modal = dateModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(EditTransferEvent.TrnTimeChange(it)) + } + ) + TrnTimeModal( + modal = timeModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(EditTransferEvent.TrnTimeChange(it)) + } + ) + + // Fee modal + FeeModal( + modal = feeModal, + fee = state.fee.value, + onRemoveFee = { + onEvent(EditTransferEvent.FeeChange(null)) + }, + onFeePercent = { + onEvent(EditTransferEvent.FeePercent(it)) + }, + onFeeChange = { + onEvent(EditTransferEvent.FeeChange(it)) + } + ) + + if (state.rate != null) { + RateModal( + modal = rateModal, + key = "transfer_rate", + rate = state.rate.rateValue, + fromCurrency = state.rate.fromCurrency, + toCurrency = state.rate.toCurrency, + onRateChange = { + onEvent(EditTransferEvent.RateChange(it)) + } + ) + } + + DeleteConfirmationModal(modal = deleteConfirmationModal) { + onEvent(EditTransferEvent.Delete) + } +} + +// region Previews +@Preview +@Composable +private fun Preview() { + IvyPreview { + UI( + state = EditTransferState( + accountFrom = dummyAccountUi( + name = "Personal Bank", + color = Blue2Dark, + ), + amountFrom = dummyCombinedValueUi(), + accountTo = dummyAccountUi(name = "Cash"), + amountTo = dummyCombinedValueUi(), + category = dummyCategoryUi(), + description = null, + timeUi = dummyTrnTimeActualUi(), + time = dummyActual(), + title = null, + fee = dummyCombinedValueUi(), + rate = TransferRateUi( + rateValueFormatted = "1.96", + rateValue = 1.95583, + fromCurrency = "EUR", + toCurrency = "BGN" + ), + + titleSuggestions = listOf("Title 1", "Title 2"), + keyboardController = KeyboardController(), + ), + onEvent = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Filled() { + IvyPreview { + UI( + state = EditTransferState( + accountFrom = dummyAccountUi( + name = "Personal Bank", + color = Blue2Dark, + ), + amountFrom = dummyCombinedValueUi(amount = 400.0), + accountTo = dummyAccountUi(name = "Cash"), + amountTo = dummyCombinedValueUi(amount = 400.0), + category = dummyCategoryUi(), + description = "Need some cash", + timeUi = dummyTrnTimeActualUi(), + time = dummyActual(), + title = "ATM Withdrawal", + fee = dummyCombinedValueUi(amount = 2.0), + rate = null, + + titleSuggestions = listOf("Title 1", "Title 2"), + keyboardController = KeyboardController(), + ), + onEvent = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferState.kt b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferState.kt new file mode 100644 index 0000000000..2120a0bce8 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferState.kt @@ -0,0 +1,30 @@ +package com.ivy.transaction.edit.transfer + +import androidx.compose.runtime.Immutable +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.transaction.TrnTime +import com.ivy.design.util.KeyboardController +import com.ivy.transaction.data.TransferRateUi + +@Immutable +data class EditTransferState( + val accountFrom: AccountUi, + val accountTo: AccountUi, + val amountFrom: CombinedValueUi, + val amountTo: CombinedValueUi, + + val category: CategoryUi?, + val timeUi: TrnTimeUi, + val time: TrnTime, + val title: String?, + val description: String?, + val fee: CombinedValueUi, + val rate: TransferRateUi?, + + val titleSuggestions: List, + + val keyboardController: KeyboardController, +) \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferViewModel.kt b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferViewModel.kt new file mode 100644 index 0000000000..91a974da0e --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/transfer/EditTransferViewModel.kt @@ -0,0 +1,366 @@ +package com.ivy.transaction.edit.transfer + +import com.ivy.common.time.provider.TimeProvider +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountByIdAct +import com.ivy.core.domain.action.account.AccountsAct +import com.ivy.core.domain.action.category.CategoryByIdAct +import com.ivy.core.domain.action.exchange.ExchangeAct +import com.ivy.core.domain.action.settings.basecurrency.BaseCurrencyAct +import com.ivy.core.domain.action.transaction.transfer.ModifyTransfer +import com.ivy.core.domain.action.transaction.transfer.TransferByBatchIdAct +import com.ivy.core.domain.action.transaction.transfer.TransferData +import com.ivy.core.domain.action.transaction.transfer.WriteTransferAct +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.domain.pure.util.combine +import com.ivy.core.domain.pure.util.flattenLatest +import com.ivy.core.domain.pure.util.takeIfNotBlank +import com.ivy.core.ui.action.BaseCurrencyRepresentationFlow +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.core.ui.action.mapping.trn.MapTrnTimeUiAct +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.Value +import com.ivy.data.transaction.TrnListItem +import com.ivy.data.transaction.TrnTime +import com.ivy.design.util.KeyboardController +import com.ivy.navigation.Navigator +import com.ivy.transaction.action.TitleSuggestionsFlow +import com.ivy.transaction.create.action.CreateTrnStepsAct +import com.ivy.transaction.create.action.WriteLastUsedAccount +import com.ivy.transaction.data.TransferRateUi +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import java.text.DecimalFormat +import javax.inject.Inject + +@HiltViewModel +class EditTransferViewModel @Inject constructor( + timeProvider: TimeProvider, + private val titleSuggestionsFlow: TitleSuggestionsFlow, + private val createTrnStepsAct: CreateTrnStepsAct, + private val mapTrnTimeUiAct: MapTrnTimeUiAct, + private val navigator: Navigator, + private val accountByIdAct: AccountByIdAct, + private val categoryByIdAct: CategoryByIdAct, + private val mapCategoryUiAct: MapCategoryUiAct, + private val baseCurrencyAct: BaseCurrencyAct, + private val writeLastUsedAccount: WriteLastUsedAccount, + private val baseCurrencyRepresentationFlow: BaseCurrencyRepresentationFlow, + private val accountsAct: AccountsAct, + private val mapAccountUiAct: MapAccountUiAct, + private val writeTransferAct: WriteTransferAct, + private val exchangeAct: ExchangeAct, + private val transferByBatchIdAct: TransferByBatchIdAct, +) : SimpleFlowViewModel() { + + private val keyboardController = KeyboardController() + + override val initialUi = EditTransferState( + accountFrom = dummyAccountUi(), + accountTo = dummyAccountUi(), + amountFrom = CombinedValueUi.initial(), + amountTo = CombinedValueUi.initial(), + category = null, + timeUi = TrnTimeUi.Actual("", ""), + time = TrnTime.Actual(timeProvider.timeNow()), + title = null, + description = null, + fee = CombinedValueUi.initial(), + rate = null, + + titleSuggestions = emptyList(), + + keyboardController = keyboardController, + ) + + // region State + private val amountFrom = MutableStateFlow(initialUi.amountFrom) + private val amountTo = MutableStateFlow(initialUi.amountTo) + private val accountFrom = MutableStateFlow(initialUi.accountFrom) + private val accountTo = MutableStateFlow(initialUi.accountTo) + private val category = MutableStateFlow(initialUi.category) + private val time = MutableStateFlow(TrnTime.Actual(timeProvider.timeNow())) + private val timeUi = MutableStateFlow(initialUi.timeUi) + private val title = MutableStateFlow(initialUi.title) + private val description = MutableStateFlow(initialUi.description) + private val fee = MutableStateFlow(initialUi.fee) + // endregion + + private lateinit var transfer: TrnListItem.Transfer + + override val uiFlow = combine( + amountFrom, amountTo, + accountFrom, accountTo, category, time, timeUi, + title, description, fee, + ) + { amountFrom, amountTo, + accountFrom, accountTo, category, time, timeUi, + title, description, fee -> + titleSuggestionsFlow( + TitleSuggestionsFlow.Input( + title = title, + categoryUi = category, + transfer = true, + ) + ).map { titleSuggestions -> + EditTransferState( + amountFrom = amountFrom, + amountTo = amountTo, + accountFrom = accountFrom, + accountTo = accountTo, + category = category, + time = time, + timeUi = timeUi, + title = title, + description = description, + fee = fee, + rate = if (amountFrom.value.currency != amountTo.value.currency && + amountFrom.value.amount > 0.0 && amountTo.value.amount > 0.0 + ) { + // e.g. 1 EUR to 1.96 BGN + // => EUR-BGN = 1.96 / 1 = 1.96 + val rateValue = amountTo.value.amount / amountFrom.value.amount + TransferRateUi( + rateValueFormatted = DecimalFormat( + "###,###,##0.${"#".repeat(6)}" + ).format(rateValue), + rateValue = rateValue, + fromCurrency = amountFrom.value.currency, + toCurrency = amountTo.value.currency, + ) + } else null, + + titleSuggestions = titleSuggestions, + + keyboardController = keyboardController, + ) + } + }.flattenLatest() + + + // region Event Handling + override suspend fun handleEvent(event: EditTransferEvent) = when (event) { + is EditTransferEvent.Initial -> handleInitial(event) + EditTransferEvent.Save -> handleSave() + EditTransferEvent.Delete -> handleDelete() + EditTransferEvent.Close -> handleClose() + is EditTransferEvent.ToAmountChange -> handleToAmountChange(event) + is EditTransferEvent.FromAmountChange -> handleFromAmountChange(event) + is EditTransferEvent.FromAccountChange -> handleFromAccountChange(event) + is EditTransferEvent.ToAccountChange -> handleToAccountChange(event) + is EditTransferEvent.FeeChange -> handleFeeChange(event) + is EditTransferEvent.FeePercent -> handleFeePercent(event) + is EditTransferEvent.TitleChange -> handleTitleChange(event) + is EditTransferEvent.DescriptionChange -> handleDescriptionChange(event) + is EditTransferEvent.CategoryChange -> handleCategoryChange(event) + is EditTransferEvent.TrnTimeChange -> handleTimeChange(event) + is EditTransferEvent.RateChange -> handleRateChange(event) + } + + private suspend fun handleInitial(event: EditTransferEvent.Initial) { + val transfer = transferByBatchIdAct(event.batchId)?.also { + this.transfer = it + } + + if (transfer == null) { + closeScreen() + return + } + + // Init UI state + amountFrom.value = CombinedValueUi( + value = transfer.from.value, + shortenFiat = false, + ) + accountFrom.value = mapAccountUiAct(transfer.from.account) + + amountTo.value = CombinedValueUi( + value = transfer.to.value, + shortenFiat = false, + ) + accountTo.value = mapAccountUiAct(transfer.to.account) + + category.value = transfer.from.category?.let { mapCategoryUiAct(it) } + time.value = transfer.time + timeUi.value = mapTrnTimeUiAct(transfer.time) + title.value = transfer.from.title + description.value = transfer.from.description + fee.value = transfer.fee?.value?.let { + CombinedValueUi( + value = it, + shortenFiat = false, + ) + } ?: CombinedValueUi( + amount = 0.0, // no fee + currency = transfer.from.value.currency, + shortenFiat = false, + ) + } + + private suspend fun handleSave() { + val accountFrom = accountByIdAct(accountFrom.value.id) ?: return + val accountTo = accountByIdAct(accountTo.value.id) ?: return + val category = category.value?.let { categoryByIdAct(it.id) } + + val data = TransferData( + accountFrom = accountFrom, + accountTo = accountTo, + amountFrom = amountFrom.value.value, + amountTo = amountTo.value.value, + category = category, + time = time.value, + title = title.value, + description = description.value, + fee = fee.value.value.takeIf { it.amount > 0.0 }, + ) + + writeTransferAct( + ModifyTransfer.edit( + batchId = transfer.batchId, + data = data + ) + ) + + closeScreen() + } + + private suspend fun handleDelete() { + writeTransferAct(ModifyTransfer.delete(transfer = transfer)) + closeScreen() + } + + private fun handleClose() { + closeScreen() + } + + private fun closeScreen() { + keyboardController.hide() + navigator.back() + } + + // region Handle value changes + + private suspend fun handleFromAmountChange(event: EditTransferEvent.FromAmountChange) { + updateFromAmount(event.amount) + } + + private suspend fun updateFromAmount( + newFromAmount: Value + ) { + val toAccount = accountByIdAct(accountTo.value.id) ?: return + + amountFrom.value = CombinedValueUi( + value = newFromAmount, + shortenFiat = false, + ) + + val rate = uiState.value.rate + if (rate != null && rate.rateValue > 0) { + // Custom exchange rate set by the user, use it + amountTo.value = CombinedValueUi( + amount = newFromAmount.amount * rate.rateValue, + currency = toAccount.currency, + shortenFiat = false, + ) + } else { + // No rate, exchange by latest rate + amountTo.value = CombinedValueUi( + value = exchangeAct( + ExchangeAct.Input( + value = newFromAmount, + outputCurrency = toAccount.currency + ) + ), + shortenFiat = false, + ) + } + } + + private fun handleToAmountChange(event: EditTransferEvent.ToAmountChange) { + amountTo.value = CombinedValueUi( + value = event.amount, + shortenFiat = false, + ) + } + + private suspend fun handleFromAccountChange(event: EditTransferEvent.FromAccountChange) { + accountFrom.value = event.account + + accountByIdAct(event.account.id)?.let { + amountFrom.value = CombinedValueUi( + amount = amountFrom.value.value.amount, + currency = it.currency, + shortenFiat = false, + ) + fee.value = CombinedValueUi( + amount = fee.value.value.amount, + currency = it.currency, + shortenFiat = false, + ) + } + } + + private suspend fun handleToAccountChange(event: EditTransferEvent.ToAccountChange) { + accountTo.value = event.account + + accountByIdAct(event.account.id)?.let { + amountTo.value = CombinedValueUi( + amount = amountTo.value.value.amount, + currency = it.currency, + shortenFiat = false, + ) + } + } + + private fun handleFeeChange(event: EditTransferEvent.FeeChange) { + fee.value = if (event.value != null) CombinedValueUi( + value = event.value, + shortenFiat = false, + ) else { + // no fee (0 fee) + CombinedValueUi( + amount = 0.0, + currency = fee.value.value.currency, + shortenFiat = false, + ) + } + } + + private fun handleFeePercent(event: EditTransferEvent.FeePercent) { + fee.value = CombinedValueUi( + amount = amountFrom.value.value.amount * event.percent, + currency = fee.value.value.currency, + shortenFiat = false, + ) + } + + private fun handleRateChange(event: EditTransferEvent.RateChange) { + amountTo.value = CombinedValueUi( + amount = amountFrom.value.value.amount * event.newRate, + currency = amountTo.value.value.currency, + shortenFiat = false, + ) + } + + private fun handleTitleChange(event: EditTransferEvent.TitleChange) { + title.value = event.title.takeIfNotBlank() + } + + private fun handleDescriptionChange(event: EditTransferEvent.DescriptionChange) { + description.value = event.description.takeIfNotBlank() + } + + private fun handleCategoryChange(event: EditTransferEvent.CategoryChange) { + category.value = event.category + } + + private suspend fun handleTimeChange(event: EditTransferEvent.TrnTimeChange) { + time.value = event.time + timeUi.value = mapTrnTimeUiAct(event.time) + } + // endregion + // endregion +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTransactionScreen.kt b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTransactionScreen.kt new file mode 100644 index 0000000000..c1c7ff5cfe --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTransactionScreen.kt @@ -0,0 +1,334 @@ +package com.ivy.transaction.edit.trn + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ivy.core.domain.pure.format.dummyCombinedValueUi +import com.ivy.core.domain.pure.format.dummyValueUi +import com.ivy.core.ui.category.pick.CategoryPickerModal +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.dummyCategoryUi +import com.ivy.core.ui.data.transaction.dummyTrnTimeActualUi +import com.ivy.core.ui.data.transaction.dummyTrnTimeDueUi +import com.ivy.core.ui.transaction.feeling +import com.ivy.core.ui.transaction.humanText +import com.ivy.core.ui.transaction.icon +import com.ivy.data.transaction.TransactionType +import com.ivy.data.transaction.dummyTrnTimeActual +import com.ivy.data.transaction.dummyTrnTimeDue +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.SwitchRow +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.rememberIvyModal +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.DeleteButton +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.l3_ivyComponents.modal.DeleteConfirmationModal +import com.ivy.design.util.IvyPreview +import com.ivy.design.util.KeyboardController +import com.ivy.design.util.keyboardPadding +import com.ivy.design.util.keyboardShownState +import com.ivy.resources.R +import com.ivy.transaction.component.* +import com.ivy.transaction.modal.DescriptionModal +import com.ivy.transaction.modal.TrnDateModal +import com.ivy.transaction.modal.TrnTimeModal +import com.ivy.transaction.modal.TrnTypeModal + +@Composable +fun BoxScope.EditTransactionScreen(trnId: String) { + val viewModel: EditTransactionViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + + state.keyboardController.wire() + + LaunchedEffect(Unit) { + viewModel.onEvent(EditTrnEvent.Initial(trnId = trnId)) + } + + UI( + state = state, + onEvent = viewModel::onEvent, + ) +} + +@Composable +private fun BoxScope.UI( + state: EditTrnState, + onEvent: (EditTrnEvent) -> Unit, +) { + val trnTypeModal = rememberIvyModal() + val dateModal = rememberIvyModal() + val timeModal = rememberIvyModal() + val accountPickerModal = rememberIvyModal() + val categoryPickerModal = rememberIvyModal() + val descriptionModal = rememberIvyModal() + val amountModal = rememberIvyModal() + val deleteConfirmationModal = rememberIvyModal() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + item(key = "toolbar") { + SpacerVer(height = 24.dp) + EditTrnScreenToolbar( + onClose = { + onEvent(EditTrnEvent.Close) + }, + trnType = state.trnType, + onChangeTrnType = { + trnTypeModal.show() + } + ) + } + item(key = "title") { + SpacerVer(height = 24.dp) + var titleFocused by remember { mutableStateOf(false) } + val keyboardShown by keyboardShownState() + TitleInput( + modifier = Modifier.onFocusChanged { + titleFocused = it.isFocused || it.hasFocus + }, + title = state.title, + focus = remember { FocusRequester() }, + onTitleChange = { onEvent(EditTrnEvent.TitleChange(it)) }, + onCta = { onEvent(EditTrnEvent.Save) } + ) + TitleSuggestions( + focused = titleFocused && keyboardShown, + suggestions = state.titleSuggestions, + onSuggestionClick = { onEvent(EditTrnEvent.TitleChange(it)) } + ) + } + item(key = "category") { + SpacerVer(height = 12.dp) + CategoryComponent( + modifier = Modifier.padding(horizontal = 16.dp), + category = state.category + ) { + categoryPickerModal.show() + } + } + item(key = "description") { + SpacerVer(height = 24.dp) + DescriptionComponent( + modifier = Modifier.padding(horizontal = 16.dp), + description = state.description + ) { + descriptionModal.show() + } + } + item(key = "trn_time") { + SpacerVer(height = 12.dp) + TrnTimeComponent( + modifier = Modifier.padding(horizontal = 16.dp), + extendedTrnTime = state.timeUi, + onDateClick = { dateModal.show() }, + onTimeClick = { timeModal.show() } + ) + } + item(key = "hidden_switch") { + SpacerVer(height = 12.dp) + SwitchRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .border(2.dp, UI.colors.primary, UI.shapes.fullyRounded), + enabled = state.hidden, + text = "Hide transaction", + onValueChange = { + onEvent(EditTrnEvent.HiddenChange(it)) + } + ) + } + item(key = "last_item_spacer") { + val keyboardShown by keyboardShownState() + if (keyboardShown) { + SpacerVer(height = keyboardPadding()) + } + // To account for "Amount Account sheet" height + SpacerVer(height = 480.dp) + } + } + + AmountAccountSheet( + amountUi = state.amount.valueUi, + amount = state.amount.value, + amountBaseCurrency = state.amountBaseCurrency, + account = state.account, + ctaText = stringResource(R.string.save), + ctaIcon = R.drawable.round_done_24, + accountPickerModal = accountPickerModal, + amountModal = amountModal, + secondaryActions = { + DeleteButton { + deleteConfirmationModal.show() + } + SpacerHor(width = 12.dp) + }, + onAccountChange = { + onEvent(EditTrnEvent.AccountChange(it)) + }, + onAmountEnter = { + onEvent(EditTrnEvent.AmountChange(it)) + }, + onCtaClick = { + onEvent(EditTrnEvent.Save) + } + ) + + Modals( + state = state, + trnTypeModal = trnTypeModal, + dateModal = dateModal, + timeModal = timeModal, + descriptionModal = descriptionModal, + categoryPickerModal = categoryPickerModal, + deleteConfirmationModal = deleteConfirmationModal, + onEvent = onEvent + ) +} + +@Composable +private fun BoxScope.Modals( + state: EditTrnState, + categoryPickerModal: IvyModal, + descriptionModal: IvyModal, + trnTypeModal: IvyModal, + dateModal: IvyModal, + timeModal: IvyModal, + deleteConfirmationModal: IvyModal, + onEvent: (EditTrnEvent) -> Unit +) { + CategoryPickerModal( + modal = categoryPickerModal, + selected = state.category, + trnType = state.trnType, + onPick = { + onEvent(EditTrnEvent.CategoryChange(it)) + } + ) + + DescriptionModal( + modal = descriptionModal, + initialDescription = state.description, + onDescriptionChange = { + onEvent(EditTrnEvent.DescriptionChange(it)) + } + ) + + TrnTypeModal( + modal = trnTypeModal, + trnType = state.trnType, + onTransactionTypeChange = { + onEvent(EditTrnEvent.TrnTypeChange(it)) + } + ) + + TrnDateModal( + modal = dateModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(EditTrnEvent.TrnTimeChange(it)) + } + ) + TrnTimeModal( + modal = timeModal, + trnTime = state.time, + onTrnTimeChange = { + onEvent(EditTrnEvent.TrnTimeChange(it)) + } + ) + + DeleteConfirmationModal(modal = deleteConfirmationModal) { + onEvent(EditTrnEvent.Delete) + } +} + +@Composable +private fun EditTrnScreenToolbar( + onClose: () -> Unit, + trnType: TransactionType, + onChangeTrnType: () -> Unit, +) { + TrnScreenToolbar( + onClose = onClose, + actions = { + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = trnType.feeling(), + text = trnType.humanText(), + icon = trnType.icon(), + onClick = onChangeTrnType, + ) + } + ) +} + + +// region Preview +@Preview +@Composable +private fun Preview_Empty() { + IvyPreview { + UI( + state = EditTrnState( + trnType = TransactionType.Income, + category = null, + description = null, + amount = dummyCombinedValueUi(), + amountBaseCurrency = null, + account = dummyAccountUi(), + title = null, + hidden = false, + + keyboardController = KeyboardController(), + titleSuggestions = emptyList(), + timeUi = dummyTrnTimeActualUi(), + time = dummyTrnTimeActual(), + ), + onEvent = {} + ) + } +} + +@Preview +@Composable +private fun Preview_Filled() { + IvyPreview { + UI( + state = EditTrnState( + trnType = TransactionType.Expense, + title = "Tabu Shisha", + category = dummyCategoryUi(), + description = "Lorem ipsum blablablabla okay good test\n1\n2\n", + amount = dummyCombinedValueUi(amount = 23.99), + amountBaseCurrency = dummyValueUi(amount = "48.23", currency = "BGN"), + account = dummyAccountUi(), + hidden = true, + + titleSuggestions = emptyList(), + keyboardController = KeyboardController(), + + timeUi = dummyTrnTimeDueUi(), + time = dummyTrnTimeDue(), + ), + onEvent = {} + ) + } +} +// endregion \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTransactionViewModel.kt b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTransactionViewModel.kt new file mode 100644 index 0000000000..2dcd14bbe9 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTransactionViewModel.kt @@ -0,0 +1,254 @@ +package com.ivy.transaction.edit.trn + +import com.ivy.common.isNotEmpty +import com.ivy.common.isNotNullOrBlank +import com.ivy.common.time.provider.TimeProvider +import com.ivy.common.toUUIDOrNull +import com.ivy.core.domain.SimpleFlowViewModel +import com.ivy.core.domain.action.account.AccountByIdAct +import com.ivy.core.domain.action.category.CategoryByIdAct +import com.ivy.core.domain.action.data.Modify +import com.ivy.core.domain.action.transaction.TrnByIdAct +import com.ivy.core.domain.action.transaction.WriteTrnsAct +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.domain.pure.util.flattenLatest +import com.ivy.core.ui.action.BaseCurrencyRepresentationFlow +import com.ivy.core.ui.action.mapping.MapCategoryUiAct +import com.ivy.core.ui.action.mapping.account.MapAccountUiAct +import com.ivy.core.ui.action.mapping.trn.MapTrnTimeUiAct +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.SyncState +import com.ivy.data.transaction.Transaction +import com.ivy.data.transaction.TransactionType +import com.ivy.data.transaction.TrnState +import com.ivy.data.transaction.TrnTime +import com.ivy.design.util.KeyboardController +import com.ivy.navigation.Navigator +import com.ivy.transaction.action.TitleSuggestionsFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class EditTransactionViewModel @Inject constructor( + timeProvider: TimeProvider, + private val mapTrnTimeUiAct: MapTrnTimeUiAct, + private val navigator: Navigator, + private val writeTrnsAct: WriteTrnsAct, + private val accountByIdAct: AccountByIdAct, + private val categoryByIdAct: CategoryByIdAct, + private val mapCategoryUiAct: MapCategoryUiAct, + private val baseCurrencyRepresentationFlow: BaseCurrencyRepresentationFlow, + private val titleSuggestionsFlow: TitleSuggestionsFlow, + private val trnByIdAct: TrnByIdAct, + private val mapAccountUiAct: MapAccountUiAct, +) : SimpleFlowViewModel() { + + private val keyboardController = KeyboardController() + + override val initialUi = EditTrnState( + trnType = TransactionType.Expense, + amount = CombinedValueUi.initial(), + amountBaseCurrency = null, + account = dummyAccountUi(), + category = null, + timeUi = TrnTimeUi.Actual("", ""), + time = TrnTime.Actual(timeProvider.timeNow()), + title = null, + description = null, + hidden = false, + + titleSuggestions = emptyList(), + keyboardController = keyboardController, + ) + + private var transaction: Transaction? = null + + // region State + private val trnType = MutableStateFlow(initialUi.trnType) + private val amount = MutableStateFlow(initialUi.amount) + private val account = MutableStateFlow(initialUi.account) + private val category = MutableStateFlow(initialUi.category) + private val time = MutableStateFlow(TrnTime.Actual(timeProvider.timeNow())) + private val timeUi = MutableStateFlow(initialUi.timeUi) + private val title = MutableStateFlow(initialUi.title) + private val description = MutableStateFlow(initialUi.description) + private val hidden = MutableStateFlow(false) + // endregion + + override val uiFlow: Flow = combine( + trnType, amountFlow(), accountCategoryFlow(), textsFlow(), othersFlow(), + ) { trnType, (amount, amountBaseCurrency), (account, category), + (title, description, titleSuggestions), (time, timeUi, hidden) -> + EditTrnState( + trnType = trnType, + amount = amount, + amountBaseCurrency = amountBaseCurrency, + account = account, + category = category, + timeUi = timeUi, + time = time, + title = title, + description = description, + hidden = hidden, + + titleSuggestions = titleSuggestions, + keyboardController = keyboardController, + ) + } + + private fun amountFlow() = amount.map { amount -> + baseCurrencyRepresentationFlow(amount.value).map { amountBaseCurrency -> + amount to amountBaseCurrency + } + }.flattenLatest() + + private fun textsFlow() = combine( + title, description, category, + ) { title, description, category -> + titleSuggestionsFlow( + TitleSuggestionsFlow.Input( + title = title, + categoryUi = category, + transfer = false, + ) + ).map { titleSuggestions -> + Triple(title, description, titleSuggestions) + } + }.flattenLatest() + + private fun accountCategoryFlow() = combine( + account, category + ) { account, category -> + account to category + } + + private fun othersFlow() = combine( + time, timeUi, hidden + ) { time, timeUi, hidden -> + Triple(time, timeUi, hidden) + } + + + // region Event Handling + override suspend fun handleEvent(event: EditTrnEvent) = when (event) { + is EditTrnEvent.Initial -> handleInitial(event) + is EditTrnEvent.Save -> handleSave() + is EditTrnEvent.Delete -> handleDelete() + is EditTrnEvent.Close -> handleClose() + is EditTrnEvent.AccountChange -> handleAccountChange(event) + is EditTrnEvent.AmountChange -> handleAmountChange(event) + is EditTrnEvent.CategoryChange -> handleCategoryChange(event) + is EditTrnEvent.DescriptionChange -> handleDescriptionChange(event) + is EditTrnEvent.TitleChange -> handleTitleChange(event) + is EditTrnEvent.TrnTimeChange -> handleTrnTimeChange(event) + is EditTrnEvent.TrnTypeChange -> handleTrnTypeChange(event) + is EditTrnEvent.HiddenChange -> handleHiddenChange(event) + } + + private suspend fun handleInitial(event: EditTrnEvent.Initial) { + val transaction = event.trnId.toUUIDOrNull()?.let { trnId -> + trnByIdAct(trnId) + } + + if (transaction != null) { + this.transaction = transaction + trnType.value = transaction.type + amount.value = CombinedValueUi( + value = transaction.value, + shortenFiat = false, + ) + account.value = mapAccountUiAct(transaction.account) + category.value = transaction.category?.let { category -> + mapCategoryUiAct(category) + } + time.value = transaction.time + timeUi.value = mapTrnTimeUiAct(transaction.time) + title.value = transaction.title + description.value = transaction.description.takeIf { it.isNotNullOrBlank() } + } else { + closeScreen() + } + } + + private suspend fun handleSave() { + val transaction = transaction ?: return + val account = accountByIdAct(account.value.id) ?: return + val category = category.value?.id?.let { categoryByIdAct(it) } + + if (amount.value.value.amount <= 0.0) return + + val updated = transaction.copy( + account = account, + category = category, + value = amount.value.value, + time = time.value, + title = title.value.takeIf { it.isNotNullOrBlank() }, + description = description.value.takeIf { it.isNotNullOrBlank() }, + type = trnType.value, + state = if (hidden.value) TrnState.Hidden else TrnState.Default, + sync = SyncState.Syncing, + ) + + writeTrnsAct(Modify.save(updated)) + closeScreen() + } + + private suspend fun handleDelete() { + val transaction = transaction ?: return + writeTrnsAct(Modify.delete(transaction.id.toString())) + closeScreen() + } + + private fun handleClose() { + closeScreen() + } + + private fun closeScreen() { + keyboardController.hide() + navigator.back() + } + + // region Handle value changes + private fun handleAccountChange(event: EditTrnEvent.AccountChange) { + account.value = event.account + } + + private fun handleCategoryChange(event: EditTrnEvent.CategoryChange) { + category.value = event.category + } + + private fun handleAmountChange(event: EditTrnEvent.AmountChange) { + amount.value = CombinedValueUi( + value = event.amount, + shortenFiat = false + ) + } + + private fun handleTitleChange(event: EditTrnEvent.TitleChange) { + title.value = event.title.takeIf { it.isNotEmpty() } + } + + private fun handleDescriptionChange(event: EditTrnEvent.DescriptionChange) { + description.value = event.description.takeIf { it.isNotEmpty() } + } + + private suspend fun handleTrnTimeChange(event: EditTrnEvent.TrnTimeChange) { + time.value = event.time + timeUi.value = mapTrnTimeUiAct(event.time) + } + + private fun handleTrnTypeChange(event: EditTrnEvent.TrnTypeChange) { + trnType.value = event.trnType + } + + private fun handleHiddenChange(event: EditTrnEvent.HiddenChange) { + hidden.value = event.hidden + } + // endregion + // endregion +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTrnEvent.kt b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTrnEvent.kt new file mode 100644 index 0000000000..80d74349c0 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTrnEvent.kt @@ -0,0 +1,23 @@ +package com.ivy.transaction.edit.trn + +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.data.Value +import com.ivy.data.transaction.TransactionType +import com.ivy.data.transaction.TrnTime + +sealed interface EditTrnEvent { + data class Initial(val trnId: String) : EditTrnEvent + object Delete : EditTrnEvent + object Save : EditTrnEvent + object Close : EditTrnEvent + + data class AmountChange(val amount: Value) : EditTrnEvent + data class TitleChange(val title: String) : EditTrnEvent + data class DescriptionChange(val description: String?) : EditTrnEvent + data class AccountChange(val account: AccountUi) : EditTrnEvent + data class CategoryChange(val category: CategoryUi?) : EditTrnEvent + data class TrnTypeChange(val trnType: TransactionType) : EditTrnEvent + data class TrnTimeChange(val time: TrnTime) : EditTrnEvent + data class HiddenChange(val hidden: Boolean) : EditTrnEvent +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTrnState.kt b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTrnState.kt new file mode 100644 index 0000000000..c852968704 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/edit/trn/EditTrnState.kt @@ -0,0 +1,29 @@ +package com.ivy.transaction.edit.trn + +import androidx.compose.runtime.Immutable +import com.ivy.core.domain.pure.format.CombinedValueUi +import com.ivy.core.domain.pure.format.ValueUi +import com.ivy.core.ui.data.CategoryUi +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.transaction.TrnTimeUi +import com.ivy.data.transaction.TransactionType +import com.ivy.data.transaction.TrnTime +import com.ivy.design.util.KeyboardController + +@Immutable +data class EditTrnState( + val trnType: TransactionType, + val amount: CombinedValueUi, + val amountBaseCurrency: ValueUi?, + val account: AccountUi, + val category: CategoryUi?, + val timeUi: TrnTimeUi, + val time: TrnTime, + val title: String?, + val description: String?, + val hidden: Boolean, + + val titleSuggestions: List, + + val keyboardController: KeyboardController, +) \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/modal/AmountModalWithAccounts.kt b/transaction/src/main/java/com/ivy/transaction/modal/AmountModalWithAccounts.kt new file mode 100644 index 0000000000..24fdcfe9a3 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/modal/AmountModalWithAccounts.kt @@ -0,0 +1,62 @@ +package com.ivy.transaction.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.dummy.dummyValue +import com.ivy.core.ui.account.pick.SingleAccountPickerRow +import com.ivy.core.ui.amount.AmountModal +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.data.Value +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.AmountModalWithAccounts( + modal: IvyModal, + amount: Value?, + account: AccountUi, + level: Int = 1, + key: String? = null, + onAddAccount: () -> Unit, + onAmountEnter: (Value) -> Unit, + onAccountChange: (AccountUi) -> Unit, +) { + AmountModal( + modal = modal, + level = level, + key = key, + initialAmount = amount, + contentAbove = { + SpacerVer(height = 24.dp) + SingleAccountPickerRow( + selected = account, + onAddAccount = onAddAccount, + onSelectedChange = onAccountChange + ) + SpacerVer(height = 12.dp) + }, + onAmountEnter = onAmountEnter + ) +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + AmountModalWithAccounts( + modal = modal, + amount = dummyValue(), + account = dummyAccountUi(), + onAddAccount = {}, + onAmountEnter = {}, + onAccountChange = {} + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/modal/DescriptionModal.kt b/transaction/src/main/java/com/ivy/transaction/modal/DescriptionModal.kt new file mode 100644 index 0000000000..e3cf9d005b --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/modal/DescriptionModal.kt @@ -0,0 +1,99 @@ +package com.ivy.transaction.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.input.InputFieldType +import com.ivy.design.l2_components.input.IvyInputField +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Positive +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l3_ivyComponents.button.DeleteButton +import com.ivy.design.util.IvyPreview +import com.ivy.transaction.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun BoxScope.DescriptionModal( + modal: IvyModal, + initialDescription: String?, + level: Int = 1, + onDescriptionChange: (String?) -> Unit, +) { + var description by remember { + mutableStateOf(initialDescription) + } + + val keyboardController = LocalSoftwareKeyboardController.current + Modal( + modal = modal, + level = level, + actions = { + if (description != null) { + DeleteButton { + onDescriptionChange(null) + modal.hide() + } + SpacerHor(width = 8.dp) + } + Positive( + text = if (description != null) + stringResource(R.string.add) else stringResource(R.string.save) + ) { + keyboardController?.hide() + onDescriptionChange(description) + modal.hide() + } + } + ) { + Title(text = stringResource(R.string.description)) + SpacerVer(height = 24.dp) + + val focus = remember { FocusRequester() } + IvyInputField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focus) + .padding(horizontal = 16.dp), + type = InputFieldType.Multiline(), + initialValue = description ?: "", + placeholder = stringResource(R.string.description_text_field_hint), + onValueChange = { + description = it + } + ) + LaunchedEffect(Unit) { + focus.requestFocus() + } + + SpacerVer(height = 24.dp) + } +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + DescriptionModal( + modal = modal, + initialDescription = "", + onDescriptionChange = {} + ) + } + +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/modal/FeeModal.kt b/transaction/src/main/java/com/ivy/transaction/modal/FeeModal.kt new file mode 100644 index 0000000000..d9eb1733b5 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/modal/FeeModal.kt @@ -0,0 +1,109 @@ +package com.ivy.transaction.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.amount.AmountModal +import com.ivy.data.Value +import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.DeleteButton +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.FeeModal( + modal: IvyModal, + fee: Value?, + level: Int = 1, + onRemoveFee: () -> Unit, + onFeePercent: (Double) -> Unit, + onFeeChange: (Value) -> Unit, +) { + AmountModal( + modal = modal, + level = level, + key = "fee", + initialAmount = fee, + contentAbove = { + val feePercents = remember { + listOf( + "0.25%" to 0.0025, + "0.5%" to 0.005, + "0.75%" to 0.0075, + "1%" to 0.01, + "1.25%" to 0.0125, + "1.5%" to 0.015, + "1.6%" to 0.016, + "1.75%" to 0.0175, + "1.8%" to 0.018, + "2%" to 0.02, + ) + } + + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + ) { + items( + items = feePercents, + key = { it.first } + ) { (percentText, percentValue) -> + SpacerHor(8.dp) + IvyButton( + size = ButtonSize.Small, + visibility = Visibility.Medium, + feeling = Feeling.Positive, + typo = UI.typoSecond.b2, + fontWeight = FontWeight.Normal, + text = percentText, + icon = null, + ) { + onFeePercent(percentValue) + } + } + item(key = "last_item_spacer") { + SpacerHor(12.dp) + } + } + }, + moreActions = { + DeleteButton { + onRemoveFee() + modal.hide() + } + SpacerHor(width = 12.dp) + }, + onAmountEnter = onFeeChange + ) +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + FeeModal( + modal = modal, + fee = null, + onRemoveFee = {}, + onFeeChange = {}, + onFeePercent = {} + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/modal/TransferAmountModal.kt b/transaction/src/main/java/com/ivy/transaction/modal/TransferAmountModal.kt new file mode 100644 index 0000000000..d3974bbfdf --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/modal/TransferAmountModal.kt @@ -0,0 +1,86 @@ +package com.ivy.transaction.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.domain.pure.dummy.dummyValue +import com.ivy.core.ui.account.pick.SingleAccountPickerRow +import com.ivy.core.ui.amount.AmountModal +import com.ivy.core.ui.data.account.AccountUi +import com.ivy.core.ui.data.account.dummyAccountUi +import com.ivy.data.Value +import com.ivy.design.l1_buildingBlocks.B2 +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.TransferAmountModal( + modal: IvyModal, + level: Int = 1, + amount: Value?, + fromAccount: AccountUi, + toAccount: AccountUi, + onAddAccount: () -> Unit, + onAmountEnter: (Value) -> Unit, + onFromAccountChange: (AccountUi) -> Unit, + onToAccountChange: (AccountUi) -> Unit, +) { + AmountModal( + modal = modal, + level = level, + initialAmount = amount, + contentAbove = { + SpacerVer(height = 16.dp) + B2( + modifier = Modifier.padding(start = 32.dp), + text = "From", + fontWeight = FontWeight.SemiBold + ) + SpacerVer(height = 4.dp) + SingleAccountPickerRow( + selected = fromAccount, + onAddAccount = onAddAccount, + onSelectedChange = onFromAccountChange + ) + SpacerVer(height = 8.dp) + B2( + modifier = Modifier.padding(start = 32.dp), + text = "To", + fontWeight = FontWeight.SemiBold + ) + SpacerVer(height = 4.dp) + SingleAccountPickerRow( + selected = toAccount, + onAddAccount = onAddAccount, + onSelectedChange = onToAccountChange + ) + SpacerVer(height = 12.dp) + }, + onAmountEnter = onAmountEnter + ) +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + TransferAmountModal( + modal = modal, + amount = dummyValue(), + fromAccount = dummyAccountUi(), + toAccount = dummyAccountUi(), + onAddAccount = {}, + onAmountEnter = {}, + onFromAccountChange = {}, + onToAccountChange = {} + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/modal/TrnDateModal.kt b/transaction/src/main/java/com/ivy/transaction/modal/TrnDateModal.kt new file mode 100644 index 0000000000..560a7d5e1e --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/modal/TrnDateModal.kt @@ -0,0 +1,123 @@ +package com.ivy.transaction.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.common.time.time +import com.ivy.core.domain.pure.dummy.dummyActual +import com.ivy.core.ui.time.picker.date.DatePickerModal +import com.ivy.data.transaction.TrnTime +import com.ivy.design.l0_system.color.Orange +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l3_ivyComponents.Feeling +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview + +private enum class TrnTimeTypeLocal { + Actual, Due +} + +@Composable +fun BoxScope.TrnDateModal( + modal: IvyModal, + trnTime: TrnTime, + level: Int = 1, + onTrnTimeChange: (TrnTime) -> Unit, +) { + var type by remember { + mutableStateOf( + when (trnTime) { + is TrnTime.Actual -> TrnTimeTypeLocal.Actual + is TrnTime.Due -> TrnTimeTypeLocal.Due + } + ) + } + + DatePickerModal( + modal = modal, + level = level, + selected = trnTime.time().toLocalDate(), + contentTop = { + SpacerVer(height = 16.dp) + TrnTimeTypeSelector( + type = type, + onTypeChange = { type = it } + ) + }, + onPick = { date -> + val time = trnTime.time().toLocalTime() + onTrnTimeChange( + when (type) { + TrnTimeTypeLocal.Actual -> TrnTime.Actual(date.atTime(time)) + TrnTimeTypeLocal.Due -> TrnTime.Due(date.atTime(time)) + } + ) + } + ) +} + +@Composable +private fun TrnTimeTypeSelector( + type: TrnTimeTypeLocal, + onTypeChange: (TrnTimeTypeLocal) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IvyButton( + modifier = Modifier.weight(1f), + size = ButtonSize.Small, + visibility = when (type) { + TrnTimeTypeLocal.Actual -> Visibility.High + TrnTimeTypeLocal.Due -> Visibility.Medium + }, + feeling = Feeling.Positive, + text = "Actual", + icon = null + ) { + onTypeChange(TrnTimeTypeLocal.Actual) + } + SpacerHor(width = 16.dp) + IvyButton( + modifier = Modifier.weight(1f), + size = ButtonSize.Small, + visibility = when (type) { + TrnTimeTypeLocal.Actual -> Visibility.Medium + TrnTimeTypeLocal.Due -> Visibility.High + }, + feeling = Feeling.Custom(Orange), + text = "Due", + icon = null + ) { + onTypeChange(TrnTimeTypeLocal.Due) + } + } +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + TrnDateModal( + modal = modal, + trnTime = dummyActual(), + onTrnTimeChange = {} + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/modal/TrnTimeModal.kt b/transaction/src/main/java/com/ivy/transaction/modal/TrnTimeModal.kt new file mode 100644 index 0000000000..007f0ec6b3 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/modal/TrnTimeModal.kt @@ -0,0 +1,60 @@ +package com.ivy.transaction.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.ivy.common.time.time +import com.ivy.core.domain.pure.dummy.dummyActual +import com.ivy.core.ui.time.picker.time.TimePickerModal +import com.ivy.data.transaction.TrnTime +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.util.IvyPreview + + +@Composable +fun BoxScope.TrnTimeModal( + modal: IvyModal, + trnTime: TrnTime, + level: Int = 1, + onTrnTimeChange: (TrnTime) -> Unit, +) { + TimePickerModal( + modal = modal, + level = level, + selected = trnTime.time().toLocalTime(), + onPick = { time -> + onTrnTimeChange( + when (trnTime) { + is TrnTime.Actual -> TrnTime.Actual( + trnTime.actual + .withHour(time.hour) + .withMinute(time.minute) + .withSecond(0) + .withNano(0) + ) + is TrnTime.Due -> TrnTime.Due( + trnTime.due + .withHour(time.hour) + .withMinute(time.minute) + .withSecond(0) + .withNano(0) + ) + } + ) + } + ) +} + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + TrnTimeModal( + modal = modal, + trnTime = dummyActual(), + onTrnTimeChange = {} + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/modal/TrnTypeModal.kt b/transaction/src/main/java/com/ivy/transaction/modal/TrnTypeModal.kt new file mode 100644 index 0000000000..7784792431 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/modal/TrnTypeModal.kt @@ -0,0 +1,92 @@ +package com.ivy.transaction.modal + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.core.ui.transaction.feeling +import com.ivy.core.ui.transaction.humanText +import com.ivy.core.ui.transaction.icon +import com.ivy.data.transaction.TransactionType +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l2_components.modal.IvyModal +import com.ivy.design.l2_components.modal.Modal +import com.ivy.design.l2_components.modal.components.Title +import com.ivy.design.l2_components.modal.previewModal +import com.ivy.design.l3_ivyComponents.Visibility +import com.ivy.design.l3_ivyComponents.button.ButtonSize +import com.ivy.design.l3_ivyComponents.button.IvyButton +import com.ivy.design.util.IvyPreview + +@Composable +fun BoxScope.TrnTypeModal( + modal: IvyModal, + trnType: TransactionType, + level: Int = 1, + onTransactionTypeChange: (TransactionType) -> Unit, +) { + var selectedTrnType by remember(trnType) { + mutableStateOf(trnType) + } + + Modal( + modal = modal, + level = level, + actions = {}, + ) { + val onSelect = { trnType: TransactionType -> + selectedTrnType = trnType + onTransactionTypeChange(trnType) + modal.hide() + } + + Title(text = "Transaction Type") + SpacerVer(height = 24.dp) + TransactionTypeButton( + trnType = TransactionType.Income, + selected = selectedTrnType, + onSelect = onSelect + ) + SpacerVer(height = 12.dp) + TransactionTypeButton( + trnType = TransactionType.Expense, + selected = selectedTrnType, + onSelect = onSelect + ) + SpacerVer(height = 24.dp) + } +} + +@Composable +private fun TransactionTypeButton( + trnType: TransactionType, + selected: TransactionType, + onSelect: (TransactionType) -> Unit +) { + IvyButton( + modifier = Modifier.padding(horizontal = 16.dp), + size = ButtonSize.Big, + visibility = if (trnType == selected) Visibility.High else Visibility.Medium, + feeling = trnType.feeling(), + text = trnType.humanText(), + icon = trnType.icon() + ) { + onSelect(trnType) + } +} + + +@Preview +@Composable +private fun Preview() { + IvyPreview { + val modal = previewModal() + TrnTypeModal( + modal = modal, + trnType = TransactionType.Income, + onTransactionTypeChange = {} + ) + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/ivy/transaction/pure/SuggestTitle.kt b/transaction/src/main/java/com/ivy/transaction/pure/SuggestTitle.kt new file mode 100644 index 0000000000..2e958d3380 --- /dev/null +++ b/transaction/src/main/java/com/ivy/transaction/pure/SuggestTitle.kt @@ -0,0 +1,46 @@ +package com.ivy.transaction.pure + +import com.ivy.common.Quadruple +import com.ivy.common.isNotNullOrBlank +import com.ivy.data.transaction.Transaction + +private const val MAX_SUGGESTIONS = 10 + +fun suggestTitle( + transactions: List, + title: String?, +): List { + val inputQuery = searchQuery(title) ?: "" + + return transactions.asSequence() // improve performance + .filter { it.title.isNotNullOrBlank() } + .groupBy { searchQuery(it.title) } + .map { (trnQuery, trns) -> + Triple(trnQuery, trns.size, trns.first()) + }.map { (trnQuery, trnsCount, trn) -> + val exactMatch = if (inputQuery.isNotBlank()) + trnQuery?.contains(inputQuery) ?: false + else false + Quadruple( + exactMatch, trnQuery, trnsCount, trn + ) + }.sortedWith( + compareByDescending { (exactMatch, _, trnsCount, _) -> + // exact matches must come first + if (exactMatch) trnsCount * 10_000 else trnsCount + } + ) + .mapNotNull { (_, _, _, trn) -> + // return the original transaction's title + trn.title + } + .filter { suggestedTitle -> + // don't show duplicated suggestions + suggestedTitle != title + } + .toList() + .take(MAX_SUGGESTIONS) +} + +fun searchQuery(query: String?): String? = + query?.trim()?.lowercase()?.takeIf { it.isNotBlank() } \ No newline at end of file diff --git a/ui-components-old/.gitignore b/ui-components-old/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/ui-components-old/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/ui-components-old/README.md b/ui-components-old/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/ui-components-old/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/ui-components-old/src/main/AndroidManifest.xml b/ui-components-old/src/main/AndroidManifest.xml deleted file mode 100644 index c5d9aeb6d2..0000000000 --- a/ui-components-old/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/CategoryList.kt b/ui-components-old/src/main/java/com/ivy/old/CategoryList.kt deleted file mode 100644 index a89cc6239e..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/CategoryList.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.ivy.wallet.ui.category - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.CategoryOld -import com.ivy.old.ListItem -import com.ivy.wallet.ui.theme.toComposeColor - -@Composable -internal fun CategoryList( - categoryList: List, - selectedCategory: CategoryOld? = null, - onSelectedCategory: (CategoryOld?) -> Unit = {} -) { - - var selectedCat: CategoryOld? by remember(selectedCategory) { - mutableStateOf(selectedCategory) - } - - val listState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() - - - Spacer(Modifier.height(16.dp)) - - LazyRow( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - state = listState - ) { - item { - Spacer(Modifier.width(24.dp)) - } - - items(items = categoryList) { category -> - ListItem( - icon = category.icon, - defaultIcon = R.drawable.ic_custom_category_s, - text = category.name, - selectedColor = category.color.toComposeColor().takeIf { - selectedCat == category - } - ) { selected -> - selectedCat = if (!selected) category else null - onSelectedCategory(selectedCat) - } - } - - item { - Spacer(Modifier.width(24.dp)) - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/ItemStatisticComponents.kt b/ui-components-old/src/main/java/com/ivy/old/ItemStatisticComponents.kt deleted file mode 100644 index 9a12ded9e6..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/ItemStatisticComponents.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.ivy.old - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.IvyCurrency -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.format - -@Composable -fun IncomeExpensesCards( - history: List, - currency: String, - income: Double, - expenses: Double, - - hasAddButtons: Boolean, - itemColor: Color, - - incomeHeaderCardClicked: () -> Unit = {}, - expenseHeaderCardClicked: () -> Unit = {}, - onAddTransaction: (TrnTypeOld) -> Unit = {}, -) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - HeaderCard( - title = com.ivy.core.ui.temp.stringRes(R.string.income_uppercase), - currencyCode = currency, - amount = income, - transactionCount = history - .filterIsInstance(TransactionOld::class.java) - .count { it.type == TrnTypeOld.INCOME }, - addButtonText = if (hasAddButtons) stringResource(R.string.add_income) else null, - isIncome = true, - - itemColor = itemColor, - onHeaderCardClicked = { incomeHeaderCardClicked() } - ) { - onAddTransaction(TrnTypeOld.INCOME) - } - - Spacer(Modifier.width(12.dp)) - - HeaderCard( - title = com.ivy.core.ui.temp.stringRes(R.string.expenses_uppercase), - currencyCode = currency, - amount = expenses, - transactionCount = history - .filterIsInstance(TransactionOld::class.java) - .count { it.type == TrnTypeOld.EXPENSE }, - addButtonText = if (hasAddButtons) stringResource(R.string.add_expense) else null, - isIncome = false, - - itemColor = itemColor, - onHeaderCardClicked = { expenseHeaderCardClicked() } - ) { - onAddTransaction(TrnTypeOld.EXPENSE) - } - - Spacer(Modifier.width(16.dp)) - } -} - -@Composable -private fun RowScope.HeaderCard( - title: String, - currencyCode: String, - amount: Double, - transactionCount: Int, - - isIncome: Boolean, - addButtonText: String?, - - itemColor: Color, - - onHeaderCardClicked: () -> Unit = {}, - onAddClick: () -> Unit -) { - val backgroundColor = if (isDarkColor(itemColor)) - MediumBlack.copy(alpha = 0.9f) else MediumWhite.copy(alpha = 0.9f) - - val contrastColor = findContrastTextColor(backgroundColor) - - Column( - modifier = Modifier - .weight(1f) - .drawColoredShadow( - color = backgroundColor, - alpha = 0.1f - ) - .background(backgroundColor, UI.shapes.rounded) - .clickable { onHeaderCardClicked() }, - ) { - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = title, - style = UI.typo.c.style( - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(12.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = amount.format(currencyCode), - style = UI.typoSecond.b1.style( - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - ) - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = IvyCurrency.fromCode(currencyCode)?.name ?: "", - style = UI.typo.b2.style( - color = contrastColor, - fontWeight = FontWeight.Normal - ) - ) - - Spacer(Modifier.height(12.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = transactionCount.toString(), - style = UI.typoSecond.b1.style( - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - ) - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = com.ivy.core.ui.temp.stringRes(R.string.transactions), - style = UI.typo.b2.style( - color = contrastColor, - fontWeight = FontWeight.Normal - ) - ) - - Spacer(Modifier.height(24.dp)) - - if (addButtonText != null) { - val addButtonBackground = if (isIncome) Green else contrastColor - IvyButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .align(Alignment.CenterHorizontally), - text = addButtonText, - shadowAlpha = 0.1f, - backgroundGradient = Gradient.solid(addButtonBackground), - textStyle = UI.typo.b2.style( - color = findContrastTextColor(addButtonBackground), - fontWeight = FontWeight.Bold - ), - wrapContentMode = false - ) { - onAddClick() - } - - Spacer(Modifier.height(12.dp)) - } - } -} diff --git a/ui-components-old/src/main/java/com/ivy/old/ItemStatisticToolbar.kt b/ui-components-old/src/main/java/com/ivy/old/ItemStatisticToolbar.kt deleted file mode 100644 index 41153611f6..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/ItemStatisticToolbar.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.ivy.old - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp -import com.ivy.base.R - -import com.ivy.wallet.ui.theme.Transparent -import com.ivy.wallet.ui.theme.components.CircleButton -import com.ivy.wallet.ui.theme.components.DeleteButton -import com.ivy.wallet.ui.theme.components.IvyOutlinedButton - -@Composable -fun ItemStatisticToolbar( - contrastColor: Color, - - onEdit: () -> Unit, - onDelete: () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - - CircleButton( - modifier = Modifier.testTag("toolbar_close"), - icon = R.drawable.ic_dismiss, - borderColor = contrastColor, - tint = contrastColor, - backgroundColor = Transparent - ) { - - } - - Spacer(Modifier.weight(1f)) - - IvyOutlinedButton( - iconStart = R.drawable.ic_edit, - text = com.ivy.core.ui.temp.stringRes(R.string.edit), - borderColor = contrastColor, - iconTint = contrastColor, - textColor = contrastColor, - solidBackground = false - ) { - onEdit() - } - - Spacer(Modifier.width(16.dp)) - - DeleteButton { - onDelete() - } - - Spacer(Modifier.width(24.dp)) - } -} diff --git a/ui-components-old/src/main/java/com/ivy/old/ListItem.kt b/ui-components-old/src/main/java/com/ivy/old/ListItem.kt deleted file mode 100644 index 0524a329b2..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/ListItem.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.ivy.old - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.color.contrastColor -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.components.ItemIconSDefaultIcon -import com.ivy.wallet.utils.thenIf - -@Deprecated("old") -@Composable -fun ListItem( - icon: String?, - @DrawableRes defaultIcon: Int, - text: String, - selectedColor: Color?, - onClick: (selected: Boolean) -> Unit -) { - val textColor = - if (selectedColor != null) contrastColor(selectedColor) else UI.colorsInverted.pure - - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .thenIf(selectedColor == null) { - border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - } - .thenIf(selectedColor != null) { - background(selectedColor!!, UI.shapes.fullyRounded) - } - .clickable( - onClick = { - onClick(selectedColor != null) - } - ), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - ItemIconSDefaultIcon( - iconName = icon, - defaultIcon = defaultIcon, - tint = textColor - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier.padding(vertical = 10.dp), - text = text, - style = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.width(12.dp)) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/OnboardingProgressSlider.kt b/ui-components-old/src/main/java/com/ivy/old/OnboardingProgressSlider.kt deleted file mode 100644 index 98f686e45c..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/OnboardingProgressSlider.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.ivy.old - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI - - - -@Composable -fun OnboardingProgressSlider( - modifier: Modifier = Modifier, - selectedStep: Int, - stepsCount: Int, - selectedColor: Color -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - for (i in 0 until stepsCount) { - val selected = selectedStep == i - Line( - width = if (selected) 48.dp else 24.dp, - color = if (selected) selectedColor else UI.colors.medium - ) - - if (i < stepsCount - 1) { - Spacer(Modifier.width(24.dp)) - } - } - } -} - -@Composable -private fun Line( - width: Dp, - color: Color -) { - Spacer( - modifier = Modifier - .size(width = width, height = 4.dp) - .background(color, UI.shapes.fullyRounded) - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/OnboardingToolbar.kt b/ui-components-old/src/main/java/com/ivy/old/OnboardingToolbar.kt deleted file mode 100644 index 6f92131247..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/OnboardingToolbar.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.ivy.old - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.IvyToolbar - -@Composable -fun OnboardingToolbar( - hasSkip: Boolean, - - onBack: () -> Unit, - onSkip: () -> Unit -) { - IvyToolbar(onBack = onBack) { - if (hasSkip) { - Spacer(Modifier.weight(1f)) - - Text( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .clickable { - onSkip() - } - .padding(all = 16.dp), //enlarge click area - text = stringResource(R.string.skip), - style = UI.typo.b2.style( - color = Gray, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.width(32.dp)) - } - } -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - OnboardingToolbar( - hasSkip = true, onBack = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/PrimaryAttributeColumn.kt b/ui-components-old/src/main/java/com/ivy/old/PrimaryAttributeColumn.kt deleted file mode 100644 index debeb6586e..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/PrimaryAttributeColumn.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.ivy.transaction_details - - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.utils.clickableNoIndication - -@Composable -fun PrimaryAttributeColumn( - @DrawableRes icon: Int, - title: String, - TitleRowExtra: (@Composable RowScope.() -> Unit)? = null, - onClick: () -> Unit, - Content: @Composable ColumnScope.() -> Unit -) { - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .clip(UI.shapes.squared) - .border(2.dp, UI.colors.medium, UI.shapes.squared) - .clickableNoIndication(onClick = onClick), - ) { - Spacer(modifier = Modifier.height(12.dp)) - - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - IvyIcon(icon = icon) - - Spacer(Modifier.width(8.dp)) - - Text( - text = title, - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - TitleRowExtra?.invoke(this) - } - - Content() - } -} - -@Preview -@Composable -private fun PreviewPrimaryAttributeColumn() { - ComponentPreview { - PrimaryAttributeColumn( - icon = R.drawable.ic_description, - title = stringResource(R.string.description), - onClick = { } - ) { - Spacer(Modifier.height(12.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = "This mode is not recommended for production use,\n" + - "as no stability/compatibility guarantees are given on\n" + - "compiler or generated code. Use it at your own risk!\n" + - "\n" + - "\n" + - "Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.\n" + - "Use '--warning-mode all' to show the individual deprecation warnings.\n" + - "See https://docs.gradle.org/7.0-rc-1/userguide/command_line_interface.html#sec:command_line_warnings", - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Medium - ) - ) - - Spacer(Modifier.height(20.dp)) - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/Suggestions.kt b/ui-components-old/src/main/java/com/ivy/old/Suggestions.kt deleted file mode 100644 index efc5ebb88a..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/Suggestions.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.ivy.old - - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.components.WrapContentRow -import com.ivy.wallet.utils.drawColoredShadow - -@Composable -fun Suggestions( - suggestions: List, - - onAddSuggestion: (Any) -> Unit, - onAddNew: () -> Unit -) { - val items = suggestions.plus(AddNew()) - - WrapContentRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - items = items, - horizontalMarginBetweenItems = 8.dp, - verticalMarginBetweenRows = 12.dp - ) { - when (it) { - is CreateAccountData -> { - Suggestion(name = it.name) { - onAddSuggestion(it) - } - } - is CreateCategoryData -> { - Suggestion(name = it.name) { - onAddSuggestion(it) - } - } - is AddNew -> { - AddNewButton { - onAddNew() - } - } - } - } -} - -@Composable -private fun Suggestion( - name: String, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .background(UI.colors.medium, UI.shapes.fullyRounded) - .clickable { - onClick() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon(icon = R.drawable.ic_plus) - - Spacer(Modifier.width(8.dp)) - - Text( - modifier = Modifier.padding(vertical = 16.dp), - text = name, - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.width(32.dp)) - } -} - -@Composable -private fun AddNewButton( - onClick: () -> Unit -) { - Row( - modifier = Modifier - .drawColoredShadow(color = UI.colorsInverted.medium) - .clip(UI.shapes.fullyRounded) - .background(UI.colorsInverted.medium, UI.shapes.fullyRounded) - .clickable { - onClick() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon( - icon = R.drawable.ic_plus, - tint = UI.colors.pure, - ) - - Spacer(Modifier.width(8.dp)) - - Text( - modifier = Modifier.padding(vertical = 16.dp), - text = stringResource(R.string.add_new), - style = UI.typo.b2.style( - color = UI.colors.pure, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.width(32.dp)) - } -} - -private class AddNew - - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - Suggestions( - suggestions = listOf( - AccountOld("Cash", color = Green.toArgb()), - AccountOld("Bank", color = Green.toArgb()), - AccountOld("Revolut", color = Green.toArgb()) - ), - onAddSuggestion = { } - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/TransactionDateTime.kt b/ui-components-old/src/main/java/com/ivy/old/TransactionDateTime.kt deleted file mode 100644 index 303da3f0cc..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/TransactionDateTime.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.ivy.transaction_details - - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.utils.formatLocalTime -import com.ivy.wallet.utils.formatNicely -import com.ivy.wallet.utils.timeNowUTC -import java.time.LocalDateTime - -@Composable -fun TransactionDateTime( - dateTime: LocalDateTime?, - dueDateTime: LocalDateTime?, - - onEditDate: () -> Unit = {}, - onEditTime: () -> Unit = {}, - onEditDateTime: () -> Unit -) { - if (dueDateTime == null || dateTime != null) { - Spacer(Modifier.height(12.dp)) - - Row( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .clip(UI.shapes.squared) - .background(UI.colors.medium, UI.shapes.squared) - .clickable { - onEditDateTime() - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon(icon = R.drawable.ic_calendar) - - Spacer(Modifier.width(8.dp)) - - Text( - text = stringResource(R.string.created_on), - style = UI.typo.b2.style( - color = UI.colors.neutral, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.width(24.dp)) - Spacer(Modifier.weight(1f)) - - Text( - text = (dateTime ?: timeNowUTC()).formatNicely( - noWeekDay = true - ), - style = UI.typoSecond.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ), - modifier = Modifier.clickable { - onEditDate() - } - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = (dateTime ?: timeNowUTC()).formatLocalTime(), - style = UI.typoSecond.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ), - modifier = Modifier.clickable { - onEditTime() - } - ) - - Spacer(modifier = Modifier.width(24.dp)) - } - } -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - TransactionDateTime( - dateTime = timeNowUTC(), - dueDateTime = null - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/component/IncomeExpensesRow.kt b/ui-components-old/src/main/java/com/ivy/old/component/IncomeExpensesRow.kt deleted file mode 100644 index b941fc555f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/component/IncomeExpensesRow.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.ivy.accounts - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 - -@Composable -fun IncomeExpensesRow( - modifier: Modifier = Modifier, - textColor: Color = UI.colorsInverted.pure, - dividerColor: Color = UI.colors.medium, - incomeLabel: String = stringResource(R.string.income_uppercase), - income: Double, - expensesLabel: String = stringResource(R.string.expenses_uppercase), - expenses: Double, - currency: String, - center: Boolean = true, - dividerSpacer: Dp? = null, -) { - Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - if (center) { - Spacer(Modifier.weight(1f)) - } - - LabelAmountColumn( - textColor = textColor, - label = incomeLabel, - amount = income, - currency = currency, - center = center - ) - - if (center) { - Spacer(Modifier.weight(1f)) - } - - if (dividerSpacer != null) { - Spacer(modifier = Modifier.width(dividerSpacer)) - } - - //Divider - Spacer( - modifier = Modifier - .width(2.dp) - .height(48.dp) - .background(dividerColor, UI.shapes.fullyRounded) - ) - - if (center) { - Spacer(Modifier.weight(1f)) - } - - if (dividerSpacer != null) { - Spacer(modifier = Modifier.width(dividerSpacer)) - } - - LabelAmountColumn( - textColor = textColor, - label = expensesLabel, - amount = expenses, - currency = currency, - center = center - ) - - if (center) { - Spacer(Modifier.weight(1f)) - } - } -} - -@Composable -private fun LabelAmountColumn( - label: String, - amount: Double, - currency: String, - textColor: Color, - center: Boolean -) { - Column( - horizontalAlignment = if (center) Alignment.CenterHorizontally else Alignment.Start - ) { - Text( - text = label, - style = UI.typo.c.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - AmountCurrencyB1( - textColor = textColor, - amount = amount, - currency = currency - ) - } - - } -} diff --git a/ui-components-old/src/main/java/com/ivy/old/component/transaction/HistoryDateDivider.kt b/ui-components-old/src/main/java/com/ivy/old/component/transaction/HistoryDateDivider.kt deleted file mode 100644 index 86e5baa95f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/component/transaction/HistoryDateDivider.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.ivy.wallet.ui.component.transaction - -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.utils.dateNowUTC -import com.ivy.wallet.utils.format -import com.ivy.wallet.utils.formatLocal -import java.time.LocalDate - -@Composable -fun HistoryDateDivider( - date: LocalDate, - spacerTop: Dp, - baseCurrency: String, - income: Double, - expenses: Double -) { - Spacer(Modifier.height(spacerTop)) - - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - val today = LocalDate.now() - - Column { - Text( - text = date.formatLocal( - if (today.year == date.year) "MMMM dd." else "MMM dd. yyy" - ), - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(4.dp)) - - Text( - text = when (date) { - today -> { - stringResource(R.string.today) - } - today.minusDays(1) -> { - stringResource(R.string.yesterday) - } - today.plusDays(1) -> { - stringResource(R.string.tomorrow) - } - else -> { - date.formatLocal("EEEE") - } - }, - style = UI.typo.c.style( - fontWeight = FontWeight.Bold - ) - ) - } - - Spacer(Modifier.weight(1f)) - - - val cashflow = income - expenses - Text( - text = "${cashflow.format(baseCurrency)} $baseCurrency", - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.Bold, - color = if (cashflow > 0) Green else Gray - ) - ) - - Spacer(Modifier.width(32.dp)) - } - - Spacer(Modifier.height(4.dp)) -} - -@Preview -@Composable -private fun Preview_Today() { - ComponentPreview { - HistoryDateDivider( - date = dateNowUTC(), - spacerTop = 32.dp, - baseCurrency = "BGN", - income = 13.50, - expenses = 256.13 - ) - } -} - -@Preview -@Composable -private fun Preview_Yesterday() { - ComponentPreview { - HistoryDateDivider( - date = dateNowUTC().minusDays(1), - spacerTop = 32.dp, - baseCurrency = "BGN", - income = 13.50, - expenses = 256.13 - ) - } -} - -@Preview -@Composable -private fun Preview_OneYear_Ago() { - ComponentPreview { - HistoryDateDivider( - date = dateNowUTC().minusYears(1), - spacerTop = 32.dp, - baseCurrency = "BGN", - income = 13.50, - expenses = 256.13 - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionCard.kt b/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionCard.kt deleted file mode 100644 index 1e4c43e38b..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionCard.kt +++ /dev/null @@ -1,662 +0,0 @@ -package com.ivy.wallet.ui.component.transaction - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.toArgb -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.base.data.AppBaseData -import com.ivy.data.AccountOld -import com.ivy.data.CategoryOld -import com.ivy.data.transaction.TransactionOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.l1_buildingBlocks.Caption -import com.ivy.design.l1_buildingBlocks.SpacerHor -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.ItemIconSDefaultIcon -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 -import com.ivy.wallet.utils.* -import java.time.LocalDateTime - - -@Composable -fun TransactionCard( - baseData: AppBaseData, - - transaction: TransactionOld, - - onPayOrGet: (TransactionOld) -> Unit, - onSkipTransaction: (TransactionOld) -> Unit = {}, - - onClick: (TransactionOld) -> Unit, -) { - Spacer(Modifier.height(12.dp)) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .clickable { - if (baseData.accounts.find { it.id == transaction.accountId } != null) { - onClick(transaction) - } - } - .background(UI.colors.medium, UI.shapes.squared) - .testTag("transaction_card") - ) { - //TODO: Optimize this - val transactionCurrency = - baseData.accounts.find { it.id == transaction.accountId }?.currency - ?: baseData.baseCurrency - - val toAccountCurrency = - baseData.accounts.find { it.id == transaction.toAccountId }?.currency - ?: baseData.baseCurrency - - Spacer(Modifier.height(20.dp)) - - TransactionHeaderRow( - transaction = transaction, - categories = baseData.categories, - accounts = baseData.accounts - ) - - if (transaction.dueDate != null) { - Spacer(Modifier.height(12.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = stringResource( - R.string.due_on, - transaction.dueDate!!.formatNicely() - ).uppercase(), - style = UI.typoSecond.c.style( - color = if (transaction.dueDate!!.isAfter(timeNowUTC())) - Orange else UI.colors.neutral, - fontWeight = FontWeight.Bold - ) - ) - } - - if (transaction.title.isNotNullOrBlank()) { - Spacer( - Modifier.height( - if (transaction.dueDate != null) 8.dp else 12.dp - ) - ) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = transaction.title!!, - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - } - - if (transaction.description.isNotNullOrBlank()) { - Spacer( - Modifier.height( - if (transaction.title.isNotNullOrBlank()) 4.dp else 8.dp - ) - ) - - Text( - text = transaction.description!!, - modifier = Modifier.padding(horizontal = 24.dp), - style = UI.typoSecond.c.style( - color = UI.colors.neutral, - fontWeight = FontWeight.Bold - ), - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } - - if (transaction.dueDate != null) { - Spacer(Modifier.height(12.dp)) - } else { - Spacer(Modifier.height(16.dp)) - } - - TypeAmountCurrency( - transactionType = transaction.type, - dueDate = transaction.dueDate, - currency = transactionCurrency, - amount = transaction.amount.toDouble() - ) - - if (transaction.type == TrnTypeOld.TRANSFER && toAccountCurrency != transactionCurrency) { - Text( - modifier = Modifier.padding(start = 68.dp), - text = "${transaction.toAmount.toDouble().format(2)} $toAccountCurrency", - style = UI.typoSecond.b2.style( - color = Gray, - fontWeight = FontWeight.Normal - ) - ) - } - - if (transaction.dueDate != null && transaction.dateTime == null) { - //Pay/Get button - Spacer(Modifier.height(16.dp)) - - val isExpense = transaction.type == TrnTypeOld.EXPENSE - Row { - IvyButton( - modifier = Modifier - .weight(1f) - .padding(start = 24.dp), - text = stringResource(R.string.skip), - wrapContentMode = false, - backgroundGradient = Gradient.solid(UI.colors.pure), - textStyle = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) { - onSkipTransaction(transaction) - } - - Spacer(Modifier.width(8.dp)) - - IvyButton( - modifier = Modifier - .weight(1f) - .padding(end = 24.dp), - text = if (isExpense) stringResource(R.string.pay) else stringResource(R.string.get), - wrapContentMode = false, - backgroundGradient = if (isExpense) gradientExpenses() else GradientGreen, - textStyle = UI.typo.b2.style( - color = if (isExpense) UI.colors.pure else White, - fontWeight = FontWeight.Bold - ) - ) { - onPayOrGet(transaction) - } - } - - } - - Spacer(Modifier.height(20.dp)) - } -} - -@Composable -private fun TransactionHeaderRow( - transaction: TransactionOld, - categories: List, - accounts: List -) { - - - if (transaction.type == TrnTypeOld.TRANSFER) { - TransferHeader( - accounts = accounts, - transaction = transaction - ) - } else { - Row( - modifier = Modifier.padding(horizontal = 20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val category: CategoryOld? = null - - if (category != null) { - TransactionBadge( - text = category.name, - backgroundColor = category.color.toComposeColor(), - icon = category.icon, - defaultIcon = R.drawable.ic_custom_category_s - ) { -// nav.navigateTo( -// ItemStatistic( -// accountId = null, -// categoryId = category.id -// ) -// ) - } - - Spacer(Modifier.width(12.dp)) - } - - val account: AccountOld = TODO() - - TransactionBadge( - text = account?.name ?: stringResource(R.string.deleted), - backgroundColor = UI.colors.pure, - icon = account?.icon, - defaultIcon = R.drawable.ic_custom_account_s - ) { - account?.let { -// nav.navigateTo( -// ItemStatistic( -// accountId = account.id, -// categoryId = null -// ) -// ) - } - } - } - } -} - -@Composable -private fun TransactionBadge( - text: String, - backgroundColor: Color, - icon: String?, - @DrawableRes - defaultIcon: Int, - - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .background(backgroundColor, UI.shapes.fullyRounded) - .clickable { - onClick() - }, - verticalAlignment = Alignment.CenterVertically - ) { - SpacerHor(width = 8.dp) - - val contrastColor = findContrastTextColor(backgroundColor) - - ItemIconSDefaultIcon( - iconName = icon, - defaultIcon = defaultIcon, - tint = contrastColor - ) - - SpacerHor(width = 4.dp) - - Caption( - text = text, - color = contrastColor, - fontWeight = FontWeight.ExtraBold - ) - - SpacerHor(width = 20.dp) - } -} - -@Composable -private fun TransferHeader( - accounts: List, - transaction: TransactionOld -) { - Row( - modifier = Modifier - .padding(horizontal = 20.dp) - .background(UI.colors.pure, UI.shapes.fullyRounded), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(8.dp)) - - val account = accounts.find { transaction.accountId == it.id } - ItemIconSDefaultIcon( - iconName = account?.icon, - defaultIcon = R.drawable.ic_custom_account_s - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier - .padding(vertical = 8.dp), - // used toString() in case of null - text = account?.name.toString(), - style = UI.typo.c.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.width(12.dp)) - - IvyIcon(icon = R.drawable.ic_arrow_right) - - Spacer(Modifier.width(12.dp)) - - val toAccount = accounts.find { transaction.toAccountId == it.id } - ItemIconSDefaultIcon( - iconName = toAccount?.icon, - defaultIcon = R.drawable.ic_custom_account_s - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier - .padding(vertical = 8.dp), - // used toString() in case of null - text = toAccount?.name.toString(), - style = UI.typo.c.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.width(20.dp)) - } -} - -@Composable -fun TypeAmountCurrency( - transactionType: TrnTypeOld, - dueDate: LocalDateTime?, - currency: String, - amount: Double -) { - - Row( - modifier = Modifier.testTag("type_amount_currency"), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - val style = when (transactionType) { - TrnTypeOld.INCOME -> { - AmountTypeStyle( - icon = R.drawable.ic_income, - gradient = GradientGreen, - iconTint = White, - textColor = Green - ) - } - TrnTypeOld.EXPENSE -> { - when { - dueDate != null && dueDate.isAfter(timeNowUTC()) -> { - //Upcoming Expense - AmountTypeStyle( - icon = R.drawable.ic_expense, - gradient = GradientOrangeRevert, - iconTint = White, - textColor = Orange - ) - } - dueDate != null && dueDate.isBefore(dateNowUTC().atStartOfDay()) -> { - //Overdue Expense - AmountTypeStyle( - icon = R.drawable.ic_overdue, - gradient = GradientRed, - iconTint = White, - textColor = Red - ) - } - else -> { - //Normal Expense - AmountTypeStyle( - icon = R.drawable.ic_expense, - gradient = Gradient.black(), - iconTint = White, - textColor = UI.colorsInverted.pure - ) - } - } - } - TrnTypeOld.TRANSFER -> { - //Transfer - AmountTypeStyle( - icon = R.drawable.ic_transfer, - gradient = GradientIvy, - iconTint = White, - textColor = Ivy - ) - } - } - - IvyIcon( - modifier = Modifier - .background(style.gradient.asHorizontalBrush(), CircleShape), - icon = style.icon, - tint = style.iconTint - ) - - Spacer(Modifier.width(12.dp)) - - AmountCurrencyB1( - amount = amount, - currency = currency, - textColor = style.textColor - ) - - Spacer(Modifier.width(24.dp)) - } -} - -private data class AmountTypeStyle( - @DrawableRes val icon: Int, - val gradient: Gradient, - val iconTint: Color, - val textColor: Color -) - -@Preview -@Composable -private fun PreviewUpcomingExpense() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val cash = AccountOld(name = "Cash", color = Green.toArgb()) - val food = CategoryOld(name = "Food", color = Green.toArgb()) - - item { - TransactionCard( - baseData = AppBaseData( - baseCurrency = "BGN", - categories = listOf(food), - accounts = listOf(cash) - ), - transaction = TransactionOld( - accountId = cash.id, - title = "Lidl pazar", - categoryId = food.id, - amount = 250.75.toBigDecimal(), - dueDate = timeNowUTC().plusDays(5), - dateTime = null, - type = TrnTypeOld.EXPENSE, - ), - onPayOrGet = {}, - ) { - - } - } - } - } -} - -@Preview -@Composable -private fun PreviewOverdueExpense() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val cash = AccountOld(name = "Cash", color = Green.toArgb()) - val food = CategoryOld(name = "Rent", color = Green.toArgb()) - - item { - TransactionCard( - baseData = AppBaseData( - baseCurrency = "BGN", - categories = listOf(food), - accounts = listOf(cash) - ), - transaction = TransactionOld( - accountId = cash.id, - title = "Rent", - categoryId = food.id, - amount = 500.0.toBigDecimal(), - dueDate = timeNowUTC().minusDays(5), - dateTime = null, - type = TrnTypeOld.EXPENSE - ), - onPayOrGet = {}, - ) { - - } - } - } - } -} - -@Preview -@Composable -private fun PreviewNormalExpense() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val cash = AccountOld(name = "Cash", color = Green.toArgb()) - val food = CategoryOld( - name = "Bitovi", - color = Orange.toArgb(), - icon = "groceries" - ) - - item { - TransactionCard( - baseData = AppBaseData( - baseCurrency = "BGN", - categories = listOf(food), - accounts = listOf(cash) - ), - transaction = TransactionOld( - accountId = cash.id, - title = "Близкия магазин", - categoryId = food.id, - amount = 32.51.toBigDecimal(), - dateTime = timeNowUTC(), - type = TrnTypeOld.EXPENSE - ), - onPayOrGet = {}, - ) { - } - } - } - } -} - -@Preview -@Composable -private fun PreviewIncome() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val cash = AccountOld(name = "DSK Bank", color = Green.toArgb()) - val category = CategoryOld(name = "Salary", color = GreenDark.toArgb()) - - item { - TransactionCard( - baseData = AppBaseData( - baseCurrency = "BGN", - categories = listOf(category), - accounts = listOf(cash) - ), - transaction = TransactionOld( - accountId = cash.id, - title = "Qredo Salary May", - categoryId = category.id, - amount = 8049.70.toBigDecimal(), - dateTime = timeNowUTC(), - type = TrnTypeOld.INCOME - ), - onPayOrGet = {}, - ) { - - } - } - } - } -} - -@Preview -@Composable -private fun PreviewTransfer() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val acc1 = AccountOld(name = "DSK Bank", color = Green.toArgb(), icon = "bank") - val acc2 = AccountOld(name = "Revolut", color = IvyDark.toArgb(), icon = "revolut") - - item { - TransactionCard( - baseData = AppBaseData( - baseCurrency = "BGN", - categories = listOf(), - accounts = listOf(acc1, acc2) - ), - transaction = TransactionOld( - accountId = acc1.id, - toAccountId = acc2.id, - title = "Top-up revolut", - amount = 1000.0.toBigDecimal(), - dateTime = timeNowUTC(), - type = TrnTypeOld.TRANSFER - ), - onPayOrGet = {}, - ) { - - } - } - } - } -} - - -@Preview -@Composable -private fun PreviewTransfer_differentCurrency() { - IvyPreview { - LazyColumn(Modifier.fillMaxSize()) { - val acc1 = AccountOld(name = "DSK Bank", color = Green.toArgb(), icon = "bank") - val acc2 = AccountOld( - name = "Revolut", - currency = "EUR", - color = IvyDark.toArgb(), - icon = "revolut" - ) - - item { - TransactionCard( - baseData = AppBaseData( - baseCurrency = "BGN", - categories = listOf(), - accounts = listOf(acc1, acc2) - ), - transaction = TransactionOld( - accountId = acc1.id, - toAccountId = acc2.id, - title = "Top-up revolut", - amount = 1000.0.toBigDecimal(), - toAmount = 510.toBigDecimal(), - dateTime = timeNowUTC(), - type = TrnTypeOld.TRANSFER - ), - onPayOrGet = {}, - ) { - - } - } - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionSectionDivider.kt b/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionSectionDivider.kt deleted file mode 100644 index 684e273ade..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionSectionDivider.kt +++ /dev/null @@ -1,199 +0,0 @@ -package com.ivy.old.component.transaction - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.* -import androidx.compose.material.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.rotate -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.l3_ivyComponents.IvyDividerDot -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Orange -import com.ivy.wallet.ui.theme.Red -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.utils.clickableNoIndication -import com.ivy.wallet.utils.format -import com.ivy.wallet.utils.springBounce - -@Composable -fun SectionDivider( - expanded: Boolean, - title: String, - titleColor: Color, - baseCurrency: String, - income: Double, - expenses: Double, - - showIncomeExpenseRow: Boolean = true, - - setExpanded: (Boolean) -> Unit -) { - Spacer(Modifier.height(24.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .clickableNoIndication { - setExpanded(!expanded) - }, - verticalAlignment = Alignment.CenterVertically - ) { - val expandIconRotation by animateFloatAsState( - targetValue = if (expanded) 0f else -180f, - animationSpec = springBounce() - ) - - Spacer(Modifier.width(24.dp)) - - Column { - Text( - modifier = Modifier.testTag("upcoming_title"), - text = title, - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold, - color = titleColor - ) - ) - - if (showIncomeExpenseRow) { - Spacer(Modifier.height(4.dp)) - - SectionDividerIncomeExpenseRow( - income = income, - expenses = expenses, - baseCurrency = baseCurrency - ) - } else { - Spacer(Modifier.height(8.dp)) - } - } - - Spacer(Modifier.weight(1f)) - - IvyIcon( - modifier = Modifier.rotate(expandIconRotation), - icon = R.drawable.ic_expandarrow - ) - - Spacer(Modifier.width(32.dp)) - } -} - -@Composable -private fun SectionDividerIncomeExpenseRow( - income: Double, - expenses: Double, - baseCurrency: String -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - if (expenses > 0) { - Text( - modifier = Modifier.testTag("upcoming_expense"), - text = "${expenses.format(baseCurrency)} $baseCurrency", - style = UI.typoSecond.c.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(com.ivy.base.R.string.expenses_lowercase), - style = UI.typo.c.style( - fontWeight = FontWeight.Normal, - color = UI.colorsInverted.pure - ) - ) - } - - if (income > 0 && expenses > 0) { - Spacer(Modifier.width(8.dp)) - - IvyDividerDot() - - Spacer(Modifier.width(8.dp)) - } - - if (income > 0) { - Text( - modifier = Modifier.testTag("upcoming_income"), - text = "${income.format(baseCurrency)} $baseCurrency", - style = UI.typoSecond.c.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colors.green - ) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(com.ivy.base.R.string.income_lowercase), - style = UI.typo.c.style( - fontWeight = FontWeight.Normal, - color = UI.colorsInverted.pure - ) - ) - } - } -} - -@Preview -@Composable -private fun Preview_Income_Expenses() { - ComponentPreview { - SectionDivider( - expanded = true, - title = "Upcoming", - titleColor = Orange, - baseCurrency = "BGN", - income = 8043.23, - expenses = 923.87 - ) { - - } - } -} - -@Preview -@Composable -private fun Preview_Expenses() { - ComponentPreview { - SectionDivider( - expanded = true, - title = "Overdue", - titleColor = Red, - baseCurrency = "BGN", - income = 0.0, - expenses = 923.87 - ) { - - } - } -} - -@Preview -@Composable -private fun Preview_Income() { - ComponentPreview { - SectionDivider( - expanded = true, - title = "Upcoming", - titleColor = Orange, - baseCurrency = "BGN", - income = 8043.23, - expenses = 0.0 - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/component/transaction/Transactions.kt b/ui-components-old/src/main/java/com/ivy/old/component/transaction/Transactions.kt deleted file mode 100644 index 457b822ce7..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/component/transaction/Transactions.kt +++ /dev/null @@ -1,353 +0,0 @@ -package com.ivy.wallet.ui.component.transaction - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.base.TransactionHistoryDateDivider -import com.ivy.base.data.AppBaseData -import com.ivy.base.data.DueSection -import com.ivy.data.transaction.TransactionOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style - - -import com.ivy.old.component.transaction.SectionDivider -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.IvyIcon - -fun LazyListScope.transactions( - baseData: AppBaseData, - - upcoming: DueSection?, - overdue: DueSection?, - history: List, - - emptyStateTitle: String = com.ivy.core.ui.temp.stringRes(R.string.no_transactions), - emptyStateText: String, - - dateDividerMarginTop: Dp? = null, - lastItemSpacer: Dp? = null, - - onPayOrGet: (TransactionOld) -> Unit, - setUpcomingExpanded: (Boolean) -> Unit, - setOverdueExpanded: (Boolean) -> Unit, - onSkipTransaction: (TransactionOld) -> Unit = {}, - onSkipAllTransactions: (List) -> Unit = {} -) { - upcomingSection( - baseData = baseData, - upcoming = upcoming, - - onPayOrGet = onPayOrGet, - onSkipTransaction = onSkipTransaction, - setExpanded = setUpcomingExpanded - ) - - overdueSection( - baseData = baseData, - overdue = overdue, - - onPayOrGet = onPayOrGet, - onSkipTransaction = onSkipTransaction, - onSkipAllTransactions = onSkipAllTransactions, - setExpanded = setOverdueExpanded - ) - - historySection( - baseData = baseData, - - history = history, - - dateDividerMarginTop = dateDividerMarginTop, - onPayOrGet = onPayOrGet - ) - - if ( - (upcoming == null || upcoming.trns.isEmpty()) && - (overdue == null || overdue.trns.isEmpty()) && - history.isEmpty() - ) { - item { - NoTransactionsEmptyState( - emptyStateTitle = emptyStateTitle, - emptyStateText = emptyStateText - ) - } - } - - scrollHackSpacer( - history = history, - upcoming = upcoming, - overdue = overdue, - lastItemSpacer = lastItemSpacer - ) -} - -private fun LazyListScope.upcomingSection( - baseData: AppBaseData, - - upcoming: DueSection?, - - onPayOrGet: (TransactionOld) -> Unit, - onSkipTransaction: (TransactionOld) -> Unit, - setExpanded: (Boolean) -> Unit -) { - if (upcoming == null) return //guard - - if (upcoming.trns.isNotEmpty()) { - item( - key = "upcoming_section" - ) { - SectionDivider( - expanded = upcoming.expanded, - setExpanded = setExpanded, - title = com.ivy.core.ui.temp.stringRes(R.string.upcoming), - titleColor = Orange, - baseCurrency = baseData.baseCurrency, - income = upcoming.stats.income.toDouble(), - expenses = upcoming.stats.expense.abs().toDouble() - ) - } - - if (upcoming.expanded) { - trnItems( - baseData = baseData, - - transactions = upcoming.trns, - - onPayOrGet = onPayOrGet, - onSkipTransaction = onSkipTransaction - ) - } - } -} - -private fun LazyListScope.overdueSection( - baseData: AppBaseData, - - overdue: DueSection?, - - onPayOrGet: (TransactionOld) -> Unit, - onSkipTransaction: (TransactionOld) -> Unit, - onSkipAllTransactions: (List) -> Unit, - setExpanded: (Boolean) -> Unit -) { - if (overdue == null) return - - if (overdue.trns.isNotEmpty()) { - item( - key = "overdue_section" - ) { - SectionDivider( - expanded = overdue.expanded, - setExpanded = setExpanded, - title = com.ivy.core.ui.temp.stringRes(R.string.overdue), - titleColor = Red, - baseCurrency = baseData.baseCurrency, - income = overdue.stats.income.toDouble(), - expenses = overdue.stats.expense.abs().toDouble() - ) - } - - if (overdue.expanded) { - item { - val isLightTheme = UI.colors.pure == White - IvyButton( - modifier = Modifier.padding(horizontal = 24.dp), - text = com.ivy.core.ui.temp.stringRes(R.string.skip_all), - wrapContentMode = false, - backgroundGradient = if (isLightTheme) Gradient(White, White) else Gradient( - Black, - Black - ), - textStyle = UI.typo.b2.style( - color = if (isLightTheme) Black else White, - fontWeight = FontWeight.Bold - ) - ) { - onSkipAllTransactions(overdue.trns) - } - } - - trnItems( - baseData = baseData, - - transactions = overdue.trns, - - onPayOrGet = onPayOrGet, - onSkipTransaction = onSkipTransaction - ) - } - } -} - -private fun LazyListScope.trnItems( - baseData: AppBaseData, - - transactions: List, - - onPayOrGet: (TransactionOld) -> Unit, - onSkipTransaction: (TransactionOld) -> Unit, -) { - items( - items = transactions, - key = { it.id } - ) { - - TransactionCard( - baseData = baseData, - - transaction = it, - onPayOrGet = onPayOrGet, - onSkipTransaction = onSkipTransaction - ) { trn -> - onTransactionClick( - - transaction = trn - ) - } - } -} - -private fun LazyListScope.historySection( - baseData: AppBaseData, - - history: List, - - dateDividerMarginTop: Dp? = null, - - onPayOrGet: (TransactionOld) -> Unit -) { - if (history.isNotEmpty()) { - items( - items = history, - key = { - when (it) { - is TransactionOld -> it.id.toString() - is TransactionHistoryDateDivider -> it.date.toString() - else -> "unknown" - } - } - ) { - when (it) { - is TransactionOld -> { - - - TransactionCard( - baseData = baseData, - - transaction = it, - onPayOrGet = onPayOrGet - ) { trn -> - onTransactionClick( - - transaction = trn - ) - } - } - - is TransactionHistoryDateDivider -> { - HistoryDateDivider( - date = it.date, - spacerTop = dateDividerMarginTop - ?: if (it == history.firstOrNull()) 24.dp else 32.dp, - baseCurrency = baseData.baseCurrency, - income = it.income, - expenses = it.expenses - ) - } - } - } - } -} - -private fun onTransactionClick( - - transaction: TransactionOld -) { -// nav.navigateTo( -// EditTransaction( -// initialTransactionId = transaction.id, -// type = transaction.type -// ) -// ) -} - -@Composable -private fun LazyItemScope.NoTransactionsEmptyState( - emptyStateTitle: String, - emptyStateText: String, -) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.height(32.dp)) - - IvyIcon( - icon = R.drawable.ic_notransactions, - tint = Gray - ) - - Spacer(Modifier.height(24.dp)) - - Text( - text = emptyStateTitle, - style = UI.typo.b1.style( - color = Gray, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = emptyStateText, - style = UI.typo.b2.style( - color = Gray, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center - ) - ) - - Spacer(Modifier.height(96.dp)) - } -} - -private fun LazyListScope.scrollHackSpacer( - history: List, - upcoming: DueSection?, - overdue: DueSection?, - - lastItemSpacer: Dp?, -) { - item { - if (lastItemSpacer != null) { - Spacer(Modifier.height(lastItemSpacer)) - } else { - //last spacer - scroll hack - val trnCount = history.size.plus( - if (upcoming != null && upcoming.expanded) upcoming.trns.size else 0 - ).plus( - if (overdue != null && overdue.expanded) overdue.trns.size else 0 - ) - if (trnCount <= 5) { - Spacer(Modifier.height(300.dp)) - } else { - Spacer(Modifier.height(150.dp)) - } - } - } -} diff --git a/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionsDividerLine.kt b/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionsDividerLine.kt deleted file mode 100644 index 1e0c00c449..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/component/transaction/TransactionsDividerLine.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.ivy.wallet.ui.component.transaction - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Divider -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI - - -@Composable -fun TransactionsDividerLine( - modifier: Modifier = Modifier, - paddingHorizontal: Dp = 24.dp -) { - Divider( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = paddingHorizontal), - color = UI.colors.medium, - thickness = 2.dp - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/IvyColors.kt b/ui-components-old/src/main/java/com/ivy/old/theme/IvyColors.kt deleted file mode 100644 index 368a5f1acb..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/IvyColors.kt +++ /dev/null @@ -1,290 +0,0 @@ -package com.ivy.wallet.ui.theme - -import androidx.annotation.ColorInt -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.core.graphics.ColorUtils -import com.ivy.design.l0_system.UI -import com.ivy.wallet.utils.densityScope - - -val White = Color(0xFFFAFAFA) -val Black = Color(0xFF111114) - -//Primary -val Ivy = Color(0xFF6B4DFF) -val Purple1 = Color(0xFFC34CFF) -val Purple2 = Color(0xFFFF4CFF) - -val Blue = Color(0xFF4CC3FF) -val Blue2 = Color(0xFF45E6E6) -val Blue3 = Color(0xFF457BE6) - -val Green = Color(0xFF14CC9E) -val Green2 = Color(0xFF45E67B) -val Green3 = Color(0xFF96E645) -val Green4 = Color(0xFFC7E62E) - -val Yellow = Color(0xFFFFEE33) - -val Orange = Color(0xFFF29F30) -val Orange2 = Color(0xFFE67B45) -val Orange3 = Color(0xFFFFC34C) - -val Red = Color(0xFFFF4060) -val Red2 = Color(0xFFE62E2E) -val Red3 = Color(0xFFFF4CA6) - - -//Light -val IvyLight = Color(0xFFD5CCFF) -val Purple1Light = Color(0xFFEECCFF) -val Purple2Light = Color(0xFFFFBFFF) - -val BlueLight = Color(0xFFB3E6FF) -val Blue2Light = Color(0xFFB3FFFF) -val Blue3Light = Color(0xFFCCDDFF) - -val GreenLight = Color(0xFFAAF2E0) -val Green2Light = Color(0xFF99FFBB) -val Green3Light = Color(0xFFCCFF99) -val Green4Light = Color(0xFFEEFF99) - -val YellowLight = Color(0xFFFFF799) - -val OrangeLight = Color(0xFFFFDEB3) -val Orange2Light = Color(0xFFFFCCB3) -val Orange3Light = Color(0xFFFFDC99) - -val RedLight = Color(0xFFFFCCD5) -val Red2Light = Color(0xFFFFB3B3) -val Red3Light = Color(0xFFFFCCE6) - - -//Dark -val IvyDark = Color(0xFF352680) -val Purple1Dark = Color(0xFF622680) -val Purple2Dark = Color(0xFF802680) - -val BlueDark = Color(0xFF266280) -val Blue2Dark = Color(0xFF227373) -val Blue3Dark = Color(0xFF223D73) - -val GreenDark = Color(0xFF0A664F) -val Green2Dark = Color(0xFF22733D) -val Green3Dark = Color(0xFF66804D) -val Green4Dark = Color(0xFF637317) - -val YellowDark = Color(0xFF807719) - -val OrangeDark = Color(0xFF734B17) -val Orange2Dark = Color(0xFF66371F) -val Orange3Dark = Color(0xFF806226) - -val RedDark = Color(0xFF801919) -val Red2Dark = Color(0xFF802030) -val Red3Dark = Color(0xFF802653) -//-------------------------------------------------------------------------------------------------- - - -val MediumBlack = Color(0xFF2B2C2D) -val Gray = Color(0xFF939199) -val MediumWhite = Color(0xFFEFEEF0) - - -val Transparent = Color(0x00000000) - -val GradientRed = Gradient(Red, Color(0xFFFF99AB)) -val GradientGreen = Gradient(Green, Color(0xFF49F2C8)) -val GradientOrange = Gradient(Orange, OrangeLight) -val GradientOrangeDark = Gradient(OrangeDark, Color(0xFFF2CD9E)) -val GradientOrangeRevert = Gradient(Color(0xFFF2CD9E), Orange) -val GradientIvy = Gradient(Ivy, Color(0xFFAA99FF)) - - -fun Color.asBrush(): Brush { - return Brush.horizontalGradient(colors = listOf(this, this)) -} - -fun Modifier.gradientCutBackgroundTop( - endY: Dp = 32.dp -) = composed { - background( - brush = Brush.verticalGradient( - colors = listOf( - Transparent, - UI.colors.pure, - ), - endY = densityScope { - endY.toPx() - } - ) - ).padding(top = 16.dp) -} - -fun Modifier.gradientCutBackgroundBottom( - paddingBottom: Dp, -) = composed { - background( - brush = Brush.verticalGradient( - colors = listOf( - UI.colors.pure, - Transparent - ), - ) - ).padding(bottom = paddingBottom) -} - -@Composable -fun pureBlur() = UI.colors.pure.copy(alpha = 0.95f) - -@Composable -fun mediumBlur() = UI.colors.medium.copy(alpha = 0.95f) - -@Composable -fun gradientExpenses() = Gradient(UI.colorsInverted.pure, UI.colors.neutral) - -data class IvyColors( - val pure: Color, - val pureInverse: Color, - - val gray: Color, - val medium: Color, - val mediumInverse: Color, - - val ivy: Color, - val ivy1: Color, - - val green: Color, - val green1: Color, - - val orange: Color, - val orange1: Color, - - val red: Color, - val red1: Color, - - val isLight: Boolean -) - -data class Gradient( - val startColor: Color, - val endColor: Color -) { - companion object { - fun from(gradient: com.ivy.design.l0_system.color.Gradient) = - Gradient(gradient.start, gradient.end) - - fun from(startColor: Int, endColor: Int?) = Gradient( - startColor = startColor.toComposeColor(), - endColor = (endColor ?: startColor).toComposeColor() - ) - - fun solid(color: Color) = Gradient(color, color) - - @Composable - fun black() = Gradient(UI.colors.neutral, UI.colorsInverted.pure) - } - - fun asHorizontalBrush() = Brush.horizontalGradient(colors = listOf(startColor, endColor)) - - fun asVerticalBrush() = Brush.verticalGradient(colors = listOf(startColor, endColor)) -} - -fun findContrastTextColor(backgroundColor: Color): Color { - return if (isDarkColor(backgroundColor.toArgb())) White else Black -} - -fun isDarkColor(color: Color): Boolean { - return isDarkColor(color.toArgb()) -} - -fun isDarkColor(@ColorInt color: Int): Boolean { - return ColorUtils.calculateLuminance(color) <= 0.5 -} - -fun Color.dynamicContrast(): Color { - val pickedColor = this.toHSVSpec() - - return when { - pickedColor.s >= 0.5f && pickedColor.v >= 0.4f -> { - //Primary - if (isDarkColor(this)) { - lighten() - } else { - darken() - } - } - pickedColor.s <= 0.5f && pickedColor.v >= 0.8f -> { - //Light - darken() - } - pickedColor.s >= 0.1f && pickedColor.v <= 0.6f -> { - //Dark - lighten() - } - else -> { - if (isDarkColor(this)) { - lighten() - } else { - darken() - } - } - } -} - -fun Color.lighten(): Color { - return this.hsv( - s = 0.3f, - v = 1f - ) -} - -fun Color.darken(): Color { - return this.hsv( - s = 0.6f, - v = 0.5f - ) -} - -fun Color.toHSVSpec(): HSVSpec { - val hsv = FloatArray(3) - val color: Int = this.toArgb() - android.graphics.Color.colorToHSV(color, hsv) - return HSVSpec(hsv[0], hsv[1], hsv[2]) -} - -data class HSVSpec( - val h: Float, - val s: Float, - val v: Float -) - -fun Color.hsv( - h: Float? = null, - s: Float, - v: Float -): Color { - val hsv = FloatArray(3) - val color: Int = this.toArgb() - android.graphics.Color.colorToHSV(color, hsv) - - if (h != null) { - hsv[0] = h - } - - hsv[1] = s - hsv[2] = v - - return Color(android.graphics.Color.HSVToColor(hsv)) -} - -fun Int.toComposeColor() = Color(this) \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/AddPrimaryAttributeButton.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/AddPrimaryAttributeButton.kt deleted file mode 100644 index afad5c0204..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/AddPrimaryAttributeButton.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview - - -@Composable -fun AddPrimaryAttributeButton( - @DrawableRes icon: Int, - text: String, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .background(UI.colors.medium, UI.shapes.squared) - .clickable(onClick = onClick) - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon(icon = icon) - - Spacer(Modifier.width(8.dp)) - - Text( - text = text, - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) - } -} - -@Preview -@Composable -private fun PreviewAddPrimaryAttributeButton() { - ComponentPreview { - AddPrimaryAttributeButton( - icon = R.drawable.ic_description, - text = "Add description" - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/BackBottomBar.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/BackBottomBar.kt deleted file mode 100644 index 11f0d1d28f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/BackBottomBar.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.wallet.ui.theme.gradientCutBackgroundTop -import com.ivy.wallet.utils.navigationBarInset -import com.ivy.wallet.utils.toDensityDp - -@Composable -fun BoxWithConstraintsScope.BackBottomBar( - bottomInset: Dp = navigationBarInset().toDensityDp(), - onBack: () -> Unit, - PrimaryAction: @Composable () -> Unit, -) { - ActionsRow( - modifier = Modifier - .align(Alignment.BottomCenter) - .gradientCutBackgroundTop() - .padding(bottom = bottomInset) - .padding(bottom = 16.dp) - ) { - Spacer(Modifier.width(20.dp)) - - CircleButton( - modifier = Modifier.rotate(180f), - icon = R.drawable.ic_arrow_right - ) { - onBack() - } - - Spacer(Modifier.weight(1f)) - - PrimaryAction() - - Spacer(Modifier.width(20.dp)) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/BalanceRow.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/BalanceRow.kt deleted file mode 100644 index d81573e910..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/BalanceRow.kt +++ /dev/null @@ -1,243 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.decimalPartFormatted -import com.ivy.wallet.utils.shortenAmount -import com.ivy.wallet.utils.shouldShortAmount -import java.text.DecimalFormat - -@Composable -fun BalanceRowMedium( - modifier: Modifier = Modifier, - textColor: Color = UI.colorsInverted.pure, - currency: String, - balance: Double, - balanceAmountPrefix: String? = null, - currencyUpfront: Boolean = true, - shortenBigNumbers: Boolean = false, - hiddenMode: Boolean = false, -) { - BalanceRow( - modifier = modifier, - - decimalPaddingTop = 8.dp, - textColor = textColor, - currency = currency, - balance = balance, - hiddenMode = hiddenMode, - spacerCurrency = 12.dp, - spacerDecimal = 8.dp, - currencyFontSize = 24.sp, - integerFontSize = 26.sp, - decimalFontSize = 11.sp, - - balanceAmountPrefix = balanceAmountPrefix, - currencyUpfront = currencyUpfront, - shortenBigNumbers = shortenBigNumbers - ) -} - -@Composable -fun BalanceRowMini( - modifier: Modifier = Modifier, - textColor: Color = UI.colorsInverted.pure, - currency: String, - balance: Double, - balanceAmountPrefix: String? = null, - currencyUpfront: Boolean = true, - shortenBigNumbers: Boolean = false, - hiddenMode: Boolean = false, -) { - BalanceRow( - modifier = modifier, - - decimalPaddingTop = 6.dp, - textColor = textColor, - currency = currency, - balance = balance, - hiddenMode = hiddenMode, - spacerCurrency = 8.dp, - spacerDecimal = 4.dp, - currencyFontSize = 20.sp, - integerFontSize = 22.sp, - decimalFontSize = 7.sp, - - balanceAmountPrefix = balanceAmountPrefix, - currencyUpfront = currencyUpfront, - shortenBigNumbers = shortenBigNumbers - ) -} - -@Composable -fun BalanceRow( - modifier: Modifier = Modifier, - currency: String, - balance: Double, - hiddenMode: Boolean = false, - - textColor: Color = UI.colorsInverted.pure, - decimalPaddingTop: Dp = 12.dp, - spacerCurrency: Dp = 12.dp, - spacerDecimal: Dp = 8.dp, - currencyFontSize: TextUnit? = null, - integerFontSize: TextUnit? = null, - decimalFontSize: TextUnit? = null, - - currencyUpfront: Boolean = true, - balanceAmountPrefix: String? = null, - shortenBigNumbers: Boolean = false, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - val shortAmount = shortenBigNumbers && shouldShortAmount(balance) - - if (currencyUpfront) { - Currency( - currency = currency, - textColor = textColor, - currencyFontSize = currencyFontSize - ) - - Spacer(Modifier.width(spacerCurrency)) - } - - val balancePrecise = balance.toBigDecimal() - - val integerPartFormatted = if (shortAmount) { - shortenAmount(balance) - } else { - DecimalFormat("###,###").format(balancePrecise.toInt()) - } - Text( - text = when { - hiddenMode -> "****" - balanceAmountPrefix != null -> "$balanceAmountPrefix$integerPartFormatted" - else -> integerPartFormatted - }, - style = if (integerFontSize == null) { - UI.typoSecond.h1.style( - fontWeight = FontWeight.ExtraBold, - color = textColor - ) - } else { - UI.typoSecond.h1.style( - fontWeight = FontWeight.ExtraBold, - color = textColor - ).copy(fontSize = integerFontSize) - } - ) - - if (!shortAmount) { - Spacer(Modifier.width(spacerDecimal)) - - Text( - modifier = Modifier - .align(Alignment.Top) - .padding(top = decimalPaddingTop), - text = if (hiddenMode) "" else decimalPartFormatted(currency, balance), - style = if (decimalFontSize == null) { - UI.typoSecond.b1.style( - fontWeight = FontWeight.Bold, - color = textColor - ) - } else { - UI.typoSecond.b1.style( - fontWeight = FontWeight.Bold, - color = textColor - ).copy(fontSize = decimalFontSize) - } - ) - } - - - if (!currencyUpfront) { - Spacer(Modifier.width(spacerCurrency)) - - Currency( - currency = currency, - textColor = textColor, - currencyFontSize = currencyFontSize - ) - } - } -} - -@Composable -private fun Currency( - currency: String, - currencyFontSize: TextUnit?, - textColor: Color, -) { - Text( - text = currency, - style = if (currencyFontSize == null) { - UI.typo.h1.style( - fontWeight = FontWeight.Light, - color = textColor - ) - } else { - UI.typo.h1.style( - fontWeight = FontWeight.Light, - color = textColor - ).copy(fontSize = currencyFontSize) - } - ) -} - -@Preview -@Composable -private fun Preview_Default() { - ComponentPreview { - BalanceRow( - textColor = UI.colorsInverted.pure, - currency = "BGN", - balance = 3520.60, - balanceAmountPrefix = null - ) - } -} - -@Preview -@Composable -private fun Preview_Medium() { - ComponentPreview { - BalanceRowMedium( - textColor = UI.colorsInverted.pure, - currency = "BGN", - balance = 3520.60, - balanceAmountPrefix = null - ) - } -} - -@Preview -@Composable -private fun Preview_Mini() { - ComponentPreview { - BalanceRowMini( - textColor = UI.colorsInverted.pure, - currency = "BGN", - balance = 3520.60, - balanceAmountPrefix = null - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/BudgetBattery.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/BudgetBattery.kt deleted file mode 100644 index 7b769f6112..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/BudgetBattery.kt +++ /dev/null @@ -1,349 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB2Row -import com.ivy.wallet.utils.format -import com.ivy.wallet.utils.thenIf -import kotlin.math.abs - -@Composable -fun BudgetBattery( - modifier: Modifier = Modifier, - currency: String, - expenses: Double, - budget: Double, - backgroundNotFilled: Color = UI.colors.pure, - onClick: (() -> Unit)? = null, -) { - if (budget == 0.0) return - val percentSpent = expenses / budget - - val textColor = when { - percentSpent <= 0.30 -> { - UI.colorsInverted.pure - } - percentSpent <= 0.50 -> { - White - } - percentSpent <= 0.75 -> { - White - } - else -> White - } - - val captionTextColor = when { - percentSpent <= 0.30 -> { - UI.colorsInverted.medium - } - percentSpent <= 0.50 -> { - White - } - percentSpent <= 0.75 -> { - White - } - else -> White - } - - Row( - modifier = modifier - .fillMaxWidth() - .clip(UI.shapes.squared) - .background(backgroundNotFilled) - .drawBehind { - drawRect( - color = when { - percentSpent <= 0.25 -> { - Green - } - percentSpent <= 0.50 -> { - Ivy - } - percentSpent <= 0.75 -> { - Orange - } - else -> Red - }, - size = size.copy( - width = (size.width * percentSpent).toFloat() - ) - ) - } - .thenIf(onClick != null) { - clickable { - onClick?.invoke() - } - } - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon( - icon = if (percentSpent > 1.0) R.drawable.ic_buffer_exceeded else R.drawable.ic_buffer_ok, - tint = textColor - ) - - Spacer(Modifier.width(16.dp)) - - Column { - Text( - text = when { - percentSpent <= 1 -> { - stringResource(R.string.left_to_spend) - } - else -> stringResource(R.string.budget_exceeded_by) - }, - style = UI.typo.c.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(4.dp)) - - AmountCurrencyB2Row( - amount = abs(budget - expenses), - currency = currency, - textColor = textColor - ) - - Spacer(Modifier.height(2.dp)) - - Text( - text = "${expenses.format(currency)}/${budget.format(currency)} $currency", - style = UI.typoSecond.c.style( - fontWeight = FontWeight.ExtraBold, - color = captionTextColor - ) - ) - } - } -} - -@Preview -@Composable -private fun Preview_budget_0() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - budget = 0.0, - expenses = 100.45, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_expenses_0() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - budget = 1000.0, - expenses = 0.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_spent_very_low() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = 5000.0, - budget = 100000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_25() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = 5000.0, - budget = 20000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_50() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = 5000.0, - budget = 10000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_75() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = 5000.0, - budget = 7500.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_90() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = 5000.0, - budget = 5500.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_100() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = 5000.0, - budget = 5000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_125() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = 5000.0, - budget = 2500.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_expenses_negative() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BudgetBattery( - modifier = Modifier.padding(horizontal = 32.dp), - expenses = -348.54, - budget = 1000.0, - currency = "BGN" - ) - } - - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/BufferBattery.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/BufferBattery.kt deleted file mode 100644 index 3859903525..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/BufferBattery.kt +++ /dev/null @@ -1,307 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB2Row -import com.ivy.wallet.utils.thenIf -import kotlin.math.abs - -@Composable -fun BufferBattery( - modifier: Modifier = Modifier, - buffer: Double, - balance: Double, - currency: String, - backgroundNotFilled: Color = UI.colors.pure, - onClick: (() -> Unit)? = null, -) { - val bufferExceeded = balance < buffer - - val leftToSpend = balance - buffer - val bufferExceededPercent = if (balance != 0.0) { - (balance - leftToSpend) / balance - } else { - 1.0 - } - - val textColor = when { - bufferExceededPercent <= 0.25 -> { - UI.colorsInverted.pure - } - bufferExceededPercent <= 0.50 -> { - White - } - bufferExceededPercent <= 0.75 -> { - White - } - else -> White - } - - Row( - modifier = modifier - .fillMaxWidth() - .clip(UI.shapes.squared) - .background(backgroundNotFilled) - .drawBehind { - drawRect( - color = when { - bufferExceededPercent <= 0.25 -> { - Green - } - bufferExceededPercent <= 0.50 -> { - Ivy - } - bufferExceededPercent <= 0.75 -> { - Orange - } - else -> Red - }, - size = size.copy( - width = (size.width * bufferExceededPercent).toFloat() - ) - ) - } - .thenIf(onClick != null) { - clickable { - onClick?.invoke() - } - } - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon( - icon = if (bufferExceeded) R.drawable.ic_buffer_exceeded else R.drawable.ic_buffer_ok, - tint = textColor - ) - - Spacer(Modifier.width(16.dp)) - - Column { - Text( - text = if (bufferExceeded) stringResource(R.string.buffer_exceeded_by) else stringResource( - R.string.left_to_spend - ), - style = UI.typo.c.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(4.dp)) - - AmountCurrencyB2Row( - amount = abs(leftToSpend), - currency = currency, - textColor = textColor - ) - } - } -} - -@Preview -@Composable -private fun Preview_buffer_0() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 0.0, - balance = 100000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_balance_0() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 0.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_very_low() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 100000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_25() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 20000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_50() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 10000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_75() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 7500.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_90() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 5500.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_100() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 5000.0, - currency = "BGN" - ) - } - - } -} - -@Preview -@Composable -private fun Preview_buffer_125() { - ComponentPreview { - Box( - modifier = Modifier - .fillMaxSize() - .background(UI.colors.medium), - contentAlignment = Alignment.Center - ) { - BufferBattery( - modifier = Modifier.padding(horizontal = 32.dp), - buffer = 5000.0, - balance = 2500.0, - currency = "BGN" - ) - } - - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/ChangeTransactionTypeModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/ChangeTransactionTypeModal.kt deleted file mode 100644 index 80c3682b2a..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/ChangeTransactionTypeModal.kt +++ /dev/null @@ -1,211 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -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.SolidColor -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.GradientGreen -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.ui.theme.modal.ModalSet -import com.ivy.wallet.ui.theme.modal.ModalTitle -import java.util.* - -@Composable -fun BoxWithConstraintsScope.ChangeTransactionTypeModal( - title: String = stringResource(R.string.set_transaction_type), - visible: Boolean, - includeTransferType: Boolean, - initialType: TrnTypeOld, - id: UUID = UUID.randomUUID(), - dismiss: () -> Unit, - onTransactionTypeChanged: (TrnTypeOld) -> Unit -) { - var transactionType by remember(id) { - mutableStateOf(initialType) - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSet { - save( - transactionType = transactionType, - onTransactionTypeChanged = onTransactionTypeChanged, - dismiss = dismiss, - ) - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle(text = title) - - Spacer(Modifier.height(32.dp)) - - TransactionTypeButton( - transactionType = TrnTypeOld.INCOME, - selected = transactionType == TrnTypeOld.INCOME, - selectedGradient = GradientGreen, - textSelectedColor = White - ) { - transactionType = TrnTypeOld.INCOME - save( - transactionType = transactionType, - onTransactionTypeChanged = onTransactionTypeChanged, - dismiss = dismiss, - ) - } - - Spacer(Modifier.height(12.dp)) - - TransactionTypeButton( - transactionType = TrnTypeOld.EXPENSE, - selected = transactionType == TrnTypeOld.EXPENSE, - selectedGradient = Gradient(UI.colorsInverted.pure, UI.colors.neutral), - textSelectedColor = UI.colors.pure - ) { - transactionType = TrnTypeOld.EXPENSE - save( - transactionType = transactionType, - onTransactionTypeChanged = onTransactionTypeChanged, - dismiss = dismiss, - ) - } - - if (includeTransferType) { - Spacer(Modifier.height(12.dp)) - - TransactionTypeButton( - transactionType = TrnTypeOld.TRANSFER, - selected = transactionType == TrnTypeOld.TRANSFER, - selectedGradient = GradientIvy, - textSelectedColor = White - ) { - transactionType = TrnTypeOld.TRANSFER - save( - transactionType = transactionType, - onTransactionTypeChanged = onTransactionTypeChanged, - dismiss = dismiss, - ) - } - } - - Spacer(Modifier.height(48.dp)) - } -} - -private fun save( - transactionType: TrnTypeOld, - onTransactionTypeChanged: (TrnTypeOld) -> Unit, - dismiss: () -> Unit -) { - onTransactionTypeChanged(transactionType) - dismiss() -} - -@Composable -private fun TransactionTypeButton( - transactionType: TrnTypeOld, - selected: Boolean, - selectedGradient: Gradient, - textSelectedColor: Color, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .clip(UI.shapes.squared) - .background( - brush = if (selected) selectedGradient.asHorizontalBrush() else SolidColor(UI.colors.medium), - shape = UI.shapes.squared - ) - .clickable { - onClick() - } - .padding(vertical = 16.dp) - .testTag("modal_type_${transactionType.name}"), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - val textColor = if (selected) textSelectedColor else UI.colorsInverted.pure - - IvyIcon( - icon = when (transactionType) { - TrnTypeOld.INCOME -> R.drawable.ic_income - TrnTypeOld.EXPENSE -> R.drawable.ic_expense - TrnTypeOld.TRANSFER -> R.drawable.ic_transfer - }, - tint = textColor - ) - - Spacer(Modifier.width(12.dp)) - - Text( - text = when (transactionType) { - TrnTypeOld.INCOME -> stringResource(R.string.income) - TrnTypeOld.EXPENSE -> stringResource(R.string.expense) - TrnTypeOld.TRANSFER -> stringResource(R.string.transfer) - }, - style = UI.typo.b1.style( - color = textColor - ) - ) - - if (selected) { - Spacer(Modifier.weight(1f)) - - IvyIcon( - icon = R.drawable.ic_check, - tint = textSelectedColor - ) - - Text( - text = stringResource(R.string.selected), - style = UI.typo.b2.style( - fontWeight = FontWeight.SemiBold, - color = textSelectedColor - ) - ) - - Spacer(Modifier.width(24.dp)) - } - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - ChangeTransactionTypeModal( - includeTransferType = true, - visible = true, - initialType = TrnTypeOld.INCOME, - dismiss = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/CircleButtons.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/CircleButtons.kt deleted file mode 100644 index 98e7d8b096..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/CircleButtons.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gradient - - -@Composable -fun CloseButton( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - CircleButton( - modifier = modifier, - icon = R.drawable.ic_dismiss, - contentDescription = "close", - onClick = onClick - ) -} - -@Composable -fun CircleButton( - modifier: Modifier = Modifier, - @DrawableRes icon: Int, - contentDescription: String = "icon", - backgroundColor: Color = UI.colors.pure, - borderColor: Color = UI.colors.medium, - tint: Color? = UI.colorsInverted.pure, - onClick: () -> Unit -) { - Icon( - modifier = modifier - .clip(CircleShape) - .clickable(onClick = onClick) - .background(backgroundColor, CircleShape) - .border(2.dp, borderColor, CircleShape) - .padding(6.dp), //enlarge click area - painter = painterResource(id = icon), - contentDescription = contentDescription, - tint = tint ?: Color.Unspecified - ) -} - -@Composable -fun CircleButtonFilled( - modifier: Modifier = Modifier, - @DrawableRes icon: Int, - contentDescription: String = "icon", - backgroundColor: Color = UI.colors.medium, - tint: Color? = UI.colorsInverted.pure, - clickAreaPadding: Dp = 8.dp, - onClick: () -> Unit -) { - Icon( - modifier = modifier - .clip(CircleShape) - .clickable(onClick = onClick) - .background(backgroundColor, CircleShape) - .padding(clickAreaPadding), //enlarge click area - painter = painterResource(id = icon), - contentDescription = contentDescription, - tint = tint ?: Color.Unspecified - ) -} - -@Composable -fun CircleButtonFilledGradient( - modifier: Modifier = Modifier, - @DrawableRes icon: Int, - contentDescription: String = "icon", - iconPadding: Dp = 8.dp, - backgroundGradient: Gradient = Gradient.solid(UI.colors.medium), - tint: Color? = UI.colorsInverted.pure, - onClick: () -> Unit -) { - Icon( - modifier = modifier - .clip(CircleShape) - .clickable(onClick = onClick) - .background(backgroundGradient.asHorizontalBrush(), CircleShape) - .padding(iconPadding), //enlarge click area - - painter = painterResource(id = icon), - contentDescription = contentDescription, - tint = tint ?: Color.Unspecified - ) -} - -@Composable -fun BackButton( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - CircleButton( - modifier = modifier, - icon = R.drawable.ic_back, - contentDescription = "back", - onClick = onClick - ) -} - -@Preview -@Composable -private fun PreviewCloseButton() { - ComponentPreview { - CloseButton { - - } - } -} - -@Preview -@Composable -private fun PreviewBackButton() { - ComponentPreview { - BackButton { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/CurrencyPicker.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/CurrencyPicker.kt deleted file mode 100644 index e461685e11..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/CurrencyPicker.kt +++ /dev/null @@ -1,402 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -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.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.IvyCurrency -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.GradientGreen -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.modal.DURATION_MODAL_ANIM -import com.ivy.wallet.utils.densityScope -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.keyboardOnlyWindowInsets -import com.ivy.wallet.utils.toLowerCaseLocal -import java.util.* - -@Composable -fun CurrencyPicker( - modifier: Modifier = Modifier, - initialSelectedCurrency: IvyCurrency?, - preselectedCurrency: IvyCurrency = IvyCurrency.getDefault(), - - includeKeyboardShownInsetSpacer: Boolean, - lastItemSpacer: Dp = 0.dp, - - onKeyboardShown: (keyboardVisible: Boolean) -> Unit = {}, - - onSelectedCurrencyChanged: (IvyCurrency) -> Unit -) { - val rootView = LocalView.current - var keyboardShown by remember { mutableStateOf(false) } - - val keyboardShownInsetDp by animateDpAsState( - targetValue = densityScope { - if (keyboardShown) keyboardOnlyWindowInsets().bottom.toDp() else 0.dp - }, - animationSpec = tween(DURATION_MODAL_ANIM) - ) - - Column( - modifier = modifier - ) { - var preselected by remember { - mutableStateOf(initialSelectedCurrency == null) - } - var selectedCurrency by remember { - mutableStateOf(initialSelectedCurrency ?: preselectedCurrency) - } - - var searchTextFieldValue by remember { mutableStateOf(TextFieldValue("")) } - - if (!keyboardShown) { - SelectedCurrencyCard( - currency = selectedCurrency, - preselected = preselected - ) - - Spacer(Modifier.height(20.dp)) - } - - SearchInput(searchTextFieldValue = searchTextFieldValue) { - searchTextFieldValue = it - } - - Spacer(Modifier.height(20.dp)) - - CurrencyList( - searchQueryLowercase = searchTextFieldValue.text.toLowerCase(Locale.getDefault()), - selectedCurrency = selectedCurrency, - lastItemSpacer = if (includeKeyboardShownInsetSpacer) - keyboardShownInsetDp + lastItemSpacer else lastItemSpacer, - ) { - preselected = false - selectedCurrency = it - onSelectedCurrencyChanged(it) - } - } -} - -@Composable -private fun SearchInput( - searchTextFieldValue: TextFieldValue, - onSetSearchTextFieldValue: (TextFieldValue) -> Unit -) { - val inputFocus = FocusRequester() - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.fullyRounded) - .border(2.dp, UI.colorsInverted.medium, UI.shapes.fullyRounded) - .clickable { - inputFocus.requestFocus() - }, - verticalAlignment = Alignment.CenterVertically - ) { - - Spacer(Modifier.width(16.dp)) - - IvyIcon( - modifier = Modifier.padding(vertical = 8.dp), - icon = R.drawable.ic_search - ) - - Spacer(Modifier.width(8.dp)) - - Box( - contentAlignment = Alignment.CenterStart - ) { - if (searchTextFieldValue.text.isEmpty()) { - //Hint - Text( - text = stringResource(R.string.search_currency), - style = UI.typo.c.style( - fontWeight = FontWeight.Bold - ) - ) - } - - val view = LocalView.current - BasicTextField( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - .focusRequester(inputFocus) - .testTag("search_input"), - value = searchTextFieldValue, - onValueChange = { - onSetSearchTextFieldValue(it.copy(it.text.trim())) - }, - textStyle = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Start - ), - singleLine = true, - cursorBrush = SolidColor(UI.colorsInverted.pure), - keyboardActions = KeyboardActions( - onDone = { - hideKeyboard(view) - } - ), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Words, - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Text - ), - ) - } - } -} - -@Composable -private fun SelectedCurrencyCard( - currency: IvyCurrency, - preselected: Boolean, -) { - Row( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .clip(UI.shapes.rounded) - .background( - brush = (if (preselected) GradientGreen else GradientIvy).asHorizontalBrush(), - shape = UI.shapes.rounded - ) - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - Column { - Text( - text = currency.name, - style = UI.typo.b2.style( - color = White, - fontWeight = FontWeight.SemiBold - ) - ) - - Spacer(Modifier.height(4.dp)) - - Text( - text = currency.code, - style = UI.typo.b1.style( - color = White, - fontWeight = FontWeight.ExtraBold - ) - ) - } - - Spacer(Modifier.weight(1f)) - - IvyIcon( - icon = R.drawable.ic_check, - tint = White - ) - - Text( - text = if (preselected) stringResource(R.string.pre_selected) else stringResource(R.string.selected), - style = UI.typo.b2.style( - color = White, - fontWeight = FontWeight.SemiBold - ) - ) - - Spacer(Modifier.width(32.dp)) - } -} - -@Composable -private fun CurrencyList( - searchQueryLowercase: String, - selectedCurrency: IvyCurrency, - lastItemSpacer: Dp, - onCurrencySelected: (IvyCurrency) -> Unit -) { - val currencies = IvyCurrency.getAvailable() - .filter { - searchQueryLowercase.isBlank() || - it.code.toLowerCaseLocal().startsWith(searchQueryLowercase) || - it.name.toLowerCaseLocal().startsWith(searchQueryLowercase) - } - .sortedBy { it.code } - .sortedBy { it.isCrypto } - - val currenciesWithLetters = mutableListOf() - - var lastFirstLetter: String? = null - for (currency in currencies) { - val firstLetter = - if (currency.isCrypto) stringResource(R.string.crypto) else currency.code.first() - .toString() - if (firstLetter != lastFirstLetter) { - currenciesWithLetters.add( - LetterDivider( - letter = firstLetter - ) - ) - lastFirstLetter = firstLetter - } - - currenciesWithLetters.add(currency) - } - - val listState = remember(searchQueryLowercase, selectedCurrency) { - LazyListState( - firstVisibleItemIndex = 0, - firstVisibleItemScrollOffset = 0 - ) - } - - - LazyColumn( - state = listState - ) { - itemsIndexed(currenciesWithLetters) { index, item -> - when (item) { - is IvyCurrency -> { - CurrencyItemCard( - currency = item, - selected = item == selectedCurrency - ) { - onCurrencySelected(item) - } - } - is LetterDivider -> { - LetterDividerItem( - spacerTop = if (index == 0) 12.dp else 32.dp, - letterDivider = item - ) - } - } - } - - if (lastItemSpacer.value > 0) { - item { - Spacer(Modifier.height(lastItemSpacer)) - } - } - } -} - -@Composable -private fun CurrencyItemCard( - currency: IvyCurrency, - selected: Boolean, - - onClick: () -> Unit, -) { - Spacer(Modifier.height(12.dp)) - - Row( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .clip(UI.shapes.squared) - .background( - color = if (selected) Ivy else UI.colors.medium, - shape = UI.shapes.squared - ) - .clickable { - onClick() - } - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - Text( - text = currency.code, - style = UI.typo.b1.style( - color = if (selected) White else UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.weight(1f)) - - Text( - text = currency.name.take(20), - style = UI.typo.b2.style( - color = if (selected) White else UI.colorsInverted.pure, - fontWeight = FontWeight.SemiBold - ) - ) - - Spacer(Modifier.width(32.dp)) - } -} - -@Composable -private fun LetterDividerItem( - spacerTop: Dp, - letterDivider: LetterDivider -) { - if (spacerTop > 0.dp) { - Spacer(Modifier.height(spacerTop)) - } - - Text( - modifier = Modifier.padding(start = 32.dp), - text = letterDivider.letter, - style = UI.typo.c.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.SemiBold - ) - ) - - Spacer(Modifier.height(6.dp)) -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - CurrencyPicker( - initialSelectedCurrency = null, - includeKeyboardShownInsetSpacer = true - ) { - - } - } -} - -private data class LetterDivider( - val letter: String -) \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/CustomExchangeRateCard.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/CustomExchangeRateCard.kt deleted file mode 100644 index ff7617e187..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/CustomExchangeRateCard.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Orange -import com.ivy.wallet.utils.format - - -@Composable -fun CustomExchangeRateCard( - modifier: Modifier = Modifier, - title: String = stringResource(R.string.exchange_rate), - fromCurrencyCode: String, - toCurrencyCode: String, - exchangeRate: Double, - onRefresh: () -> Unit = {}, - onClick: () -> Unit -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .background(UI.colors.medium, UI.shapes.squared) - .clickable(onClick = onClick) - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon(icon = R.drawable.ic_currency) - - Spacer(Modifier.width(8.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - Text( - text = title, - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = fromCurrencyCode, - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = Orange - ) - ) - IvyIcon(icon = R.drawable.ic_arrow_right, tint = Orange) - Text( - text = "$toCurrencyCode \t\t:\t\t", - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.ExtraBold, - color = Orange - ) - ) - Text( - text = exchangeRate.format(4), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.ExtraBold, - color = Orange - ) - ) - } - } - IvyIcon( - icon = R.drawable.ic_refresh, - modifier = Modifier - .padding(end = 16.dp) - .clickable { - onRefresh() - }) - } -} - -@Preview -@Composable -private fun Preview_OneTime() { - ComponentPreview { - CustomExchangeRateCard( - fromCurrencyCode = "INR", - toCurrencyCode = "EUR", - exchangeRate = (86.2) - ) { - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/DeleteButton.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/DeleteButton.kt deleted file mode 100644 index 87e5134991..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/DeleteButton.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.wallet.ui.theme.GradientRed -import com.ivy.wallet.ui.theme.White - -@Composable -fun DeleteButton( - modifier: Modifier = Modifier, - hasShadow: Boolean = true, - onClick: () -> Unit, -) { - IvyCircleButton( - modifier = modifier - .size(48.dp) - .testTag("delete_button"), - backgroundPadding = 6.dp, - icon = R.drawable.ic_delete, - backgroundGradient = GradientRed, - enabled = true, - hasShadow = hasShadow, - tint = White, - onClick = onClick - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/GradientCut.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/GradientCut.kt deleted file mode 100644 index 7682cd0da9..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/GradientCut.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import com.ivy.design.l0_system.UI -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.Transparent -import com.ivy.wallet.utils.thenIf - - -@Composable -fun BoxWithConstraintsScope.GradientCutBottom( - height: Dp = 96.dp, - alpha: Float = 1f, - zIndex: Float? = null -) { - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(height) - .thenIf(zIndex != null) { - zIndex(zIndex!!) - } - .background(Gradient(Transparent, UI.colors.pure).asVerticalBrush()) - .align(Alignment.BottomCenter) - .alpha(alpha = alpha) - ) -} - -@Composable -fun BoxWithConstraintsScope.GradientCutTop( - modifier: Modifier = Modifier, - height: Dp, -) { - Spacer( - modifier = modifier - .fillMaxWidth() - .height(height) - .background(Gradient(UI.colors.pure, Transparent).asVerticalBrush()) - .align(Alignment.TopCenter) - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IntervalPickerRow.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IntervalPickerRow.kt deleted file mode 100644 index a0404ce3cf..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IntervalPickerRow.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.forDisplay -import com.ivy.data.planned.IntervalType -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.utils.capitalizeLocal -import com.ivy.wallet.utils.isNotNullOrBlank -import com.ivy.wallet.utils.selectEndTextFieldValue - -@Composable -fun IntervalPickerRow( - intervalN: Int, - intervalType: IntervalType, - - onSetIntervalN: (Int) -> Unit, - onSetIntervalType: (IntervalType) -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - var interNTextFieldValue by remember(intervalN) { - mutableStateOf(selectEndTextFieldValue(intervalN.toString())) - } - - val validInput = intervalN > 0 && interNTextFieldValue.text.isNotNullOrBlank() - - IvyNumberTextField( - modifier = Modifier - .background( - brush = if (validInput) - GradientIvy.asHorizontalBrush() else Gradient - .solid(UI.colors.medium) - .asHorizontalBrush(), - shape = UI.shapes.fullyRounded - ) - .padding(vertical = 12.dp), - value = interNTextFieldValue, - textColor = if (validInput) White else UI.colorsInverted.pure, - hint = "0" - ) { - if (it.text != interNTextFieldValue.text) { - try { - onSetIntervalN(it.text.toInt()) - } catch (e: Exception) { - } - } - interNTextFieldValue = it - } - - Spacer(Modifier.width(12.dp)) - - IntervalTypeSelector( - intervalN = intervalN, - intervalType = intervalType - ) { - onSetIntervalType(it) - } - - Spacer(Modifier.width(24.dp)) - } -} - -@Composable -private fun RowScope.IntervalTypeSelector( - intervalN: Int, - intervalType: IntervalType, - - onSetIntervalType: (IntervalType) -> Unit -) { - - Row( - modifier = Modifier - .weight(1f) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - IvyIcon( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .clickable { - onSetIntervalType( - when (intervalType) { - IntervalType.DAY -> IntervalType.YEAR - IntervalType.WEEK -> IntervalType.DAY - IntervalType.MONTH -> IntervalType.WEEK - IntervalType.YEAR -> IntervalType.MONTH - } - ) - } - .padding(all = 8.dp) - .rotate(-180f), - icon = R.drawable.ic_arrow_right, - contentDescription = "interval_type_arrow_left" - ) - - Spacer(Modifier.weight(1f)) - - Text( - text = intervalType.forDisplay(intervalN).capitalizeLocal(), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) - - Spacer(Modifier.weight(1f)) - - IvyIcon( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .clickable { - onSetIntervalType( - when (intervalType) { - IntervalType.DAY -> IntervalType.WEEK - IntervalType.WEEK -> IntervalType.MONTH - IntervalType.MONTH -> IntervalType.YEAR - IntervalType.YEAR -> IntervalType.DAY - } - ) - } - .padding(all = 8.dp), - icon = R.drawable.ic_arrow_right, - contentDescription = "interval_type_arrow_right" - ) - - Spacer(Modifier.width(20.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - IntervalPickerRow( - intervalN = 1, - intervalType = IntervalType.WEEK, - onSetIntervalN = {} - ) { - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/ItemIcon.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/ItemIcon.kt deleted file mode 100644 index b777186851..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/ItemIcon.kt +++ /dev/null @@ -1,336 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import android.content.Context -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview -import com.ivy.design.util.thenWhen -import com.ivy.wallet.utils.toLowerCaseLocal - -@Deprecated("use IvyIcon.ItemIcon()") -@Composable -fun ItemIconL( - modifier: Modifier = Modifier, - iconName: String?, - tint: Color = UI.colorsInverted.pure, - iconContentScale: ContentScale? = null, - Default: (@Composable () -> Unit)? = null -) { - ItemIcon( - modifier = modifier - .size(64.dp), - size = "l", - iconName = iconName, - tint = tint, - iconContentScale = iconContentScale, - Default = Default - ) -} - -@Deprecated("use IvyIcon.ItemIcon()") -@Composable -fun ItemIconMDefaultIcon( - modifier: Modifier = Modifier, - iconName: String?, - tint: Color = UI.colorsInverted.pure, - @DrawableRes defaultIcon: Int -) { - ItemIconM( - modifier = modifier, - iconName = iconName, - tint = tint, - Default = { - Image( - modifier = modifier, - painter = painterResource(id = defaultIcon), - colorFilter = ColorFilter.tint(tint), - contentDescription = "item icon" - ) - } - ) -} - -@Deprecated("use IvyIcon.ItemIcon()") -@Composable -fun ItemIconM( - modifier: Modifier = Modifier, - iconName: String?, - tint: Color = UI.colorsInverted.pure, - iconContentScale: ContentScale? = null, - Default: (@Composable () -> Unit)? = null -) { - ItemIcon( - modifier = modifier - .size(48.dp), - size = "m", - iconName = iconName, - tint = tint, - iconContentScale = iconContentScale, - Default = Default - ) -} - -@Deprecated("use IvyIcon.ItemIcon()") -@Composable -fun ItemIconSDefaultIcon( - modifier: Modifier = Modifier, - iconName: String?, - tint: Color = UI.colorsInverted.pure, - @DrawableRes defaultIcon: Int -) { - ItemIconS( - modifier = modifier, - iconName = iconName, - tint = tint, - Default = { - Image( - modifier = modifier, - painter = painterResource(id = defaultIcon), - colorFilter = ColorFilter.tint(tint), - contentDescription = "item icon" - ) - } - ) -} - -@Deprecated("use IvyIcon.ItemIcon()") -@Composable -fun ItemIconS( - modifier: Modifier = Modifier, - iconName: String?, - tint: Color = UI.colorsInverted.pure, - iconContentScale: ContentScale? = null, - Default: (@Composable () -> Unit)? = null -) { - ItemIcon( - modifier = modifier - .size(32.dp), - size = "s", - iconName = iconName, - tint = tint, - iconContentScale = iconContentScale, - Default = Default - ) -} - -@Deprecated("use IvyIcon.ItemIcon()") -@Composable -private fun ItemIcon( - modifier: Modifier = Modifier, - iconName: String?, - size: String, - tint: Color = UI.colorsInverted.pure, - iconContentScale: ContentScale? = null, - Default: (@Composable () -> Unit)? = null -) { - val context = LocalContext.current - val iconInfo = getCustomIconId( - context = context, - iconName = iconName, - size = size - ) - - if (iconInfo != null) { - Image( - modifier = modifier - .thenWhen { - if (!iconInfo.newFormat) { - //do nothing for the old format of icons - return@thenWhen this - } - - when (iconInfo.style) { - IconStyle.L -> - //64.dp - 48.dp = 16.dp / 4 = 4.dp - this.padding(all = 4.dp) - IconStyle.M -> - //48.dp - 32.dp = 16.dp / 4 = 4.dp - this.padding(all = 4.dp) - IconStyle.S -> - //32.dp - 24.dp = 8.dp / 4 = 2.dp - //2.dp is too small padding - this.padding(all = 4.dp) - IconStyle.UNKNOWN -> this - } - }, - painter = painterResource(id = iconInfo.iconId), - colorFilter = ColorFilter.tint(tint), - alignment = Alignment.Center, - contentScale = iconContentScale ?: if (iconInfo.newFormat) - ContentScale.Fit else ContentScale.None, - contentDescription = iconName ?: "item icon" - ) - } else { - Default?.invoke() - } -} - -@Deprecated("use IvyIcon.ItemIcon()") -@DrawableRes -@Composable -fun getCustomIconIdS( - iconName: String?, - @DrawableRes defaultIcon: Int -): Int { - val context = LocalContext.current - return getCustomIconId( - context = context, - iconName = iconName, - size = "s" - )?.iconId ?: defaultIcon -} - -@Deprecated("use IvyIcon.ItemIcon()") -@DrawableRes -@Composable -fun getCustomIconIdM( - iconName: String?, - @DrawableRes defaultIcon: Int -): Int { - val context = LocalContext.current - return getCustomIconId( - context = context, - iconName = iconName, - size = "m" - )?.iconId ?: defaultIcon -} - -@Deprecated("use IvyIcon.ItemIcon()") -@DrawableRes -@Composable -fun getCustomIconIdL( - iconName: String?, - @DrawableRes defaultIcon: Int -): Int { - val context = LocalContext.current - return getCustomIconId( - context = context, - iconName = iconName, - size = "l" - )?.iconId ?: defaultIcon -} - -fun getCustomIconId( - context: Context, - iconName: String?, - size: String, -): IconInfo? { - val iconStyle = when (size) { - "l" -> IconStyle.L - "m" -> IconStyle.M - "s" -> IconStyle.S - else -> IconStyle.UNKNOWN - } - - return iconName?.let { - try { - val iconNameNormalized = iconName - .replace(" ", "") - .trim() - .toLowerCaseLocal() - - val itemId = context.resources.getIdentifier( - "ic_custom_${iconNameNormalized}_${size}", - "drawable", - context.packageName - ).takeIf { it != 0 } - - itemId?.let { nonNullId -> - IconInfo( - iconId = nonNullId, - style = iconStyle, - newFormat = false - ) - } ?: fallbackToNewIconFormat( - context = context, - iconName = iconName, - iconStyle = iconStyle - ) - } catch (e: Exception) { - fallbackToNewIconFormat( - context = context, - iconName = iconName, - iconStyle = iconStyle - ) - } - } -} - -data class IconInfo( - @DrawableRes - val iconId: Int, - val style: IconStyle, - val newFormat: Boolean -) - -enum class IconStyle { - L, M, S, UNKNOWN -} - -fun fallbackToNewIconFormat( - iconStyle: IconStyle, - context: Context, - iconName: String?, -): IconInfo? { - return iconName?.let { - try { - val iconNameNormalized = iconName - .replace(" ", "") - .trim() - .toLowerCaseLocal() - - val iconId = context.resources.getIdentifier( - iconNameNormalized, - "drawable", - context.packageName - ).takeIf { it != 0 } - - iconId?.let { nonNullId -> - IconInfo( - iconId = nonNullId, - style = iconStyle, - newFormat = true - ) - } - } catch (e: Exception) { - null - } - } -} - -@Preview -@Composable -private fun Preview_L() { - ComponentPreview { - ItemIconL(iconName = "dna") - } -} - -@Preview -@Composable -private fun Preview_M() { - ComponentPreview { - ItemIconM(iconName = "document") - } -} - -@Preview -@Composable -private fun Preview_S() { - ComponentPreview { - ItemIconS(iconName = "fooddrink") - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyBasicTextField.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyBasicTextField.kt deleted file mode 100644 index 15172e8d44..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyBasicTextField.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.* -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank -import com.ivy.wallet.utils.selectEndTextFieldValue - - -@Composable -fun IvyBasicTextField( - modifier: Modifier = Modifier, - value: TextFieldValue, - textColor: Color = UI.colorsInverted.pure, - hint: String?, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done, - capitalization = KeyboardCapitalization.Sentences - ), - keyboardActions: KeyboardActions? = null, - onValueChanged: (TextFieldValue) -> Unit -) { - val isEmpty = value.text.isBlank() - - Box( - modifier = modifier, - contentAlignment = Alignment.CenterStart - ) { - if (isEmpty && hint.isNotNullOrBlank()) { - Text( - modifier = Modifier, - text = hint!!, - style = UI.typo.b2.style( - color = UI.colors.neutral, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Start - ), - ) - } - - val view = LocalView.current - BasicTextField( - modifier = Modifier - .testTag("base_input"), - value = value, - onValueChange = onValueChanged, - textStyle = UI.typo.b2.style( - fontWeight = FontWeight.SemiBold, - color = UI.colorsInverted.pure, - textAlign = TextAlign.Start - ), - singleLine = false, - cursorBrush = SolidColor(UI.colorsInverted.pure), - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions ?: KeyboardActions( - onDone = { - hideKeyboard(view) - } - ) - ) - } -} - -@Preview -@Composable -private fun Preview_Hint() { - ComponentPreview { - IvyBasicTextField( - value = selectEndTextFieldValue(""), - hint = "Search transactions", - onValueChanged = {} - ) - } -} - -@Preview -@Composable -private fun Preview_Filled() { - ComponentPreview { - IvyBasicTextField( - value = selectEndTextFieldValue("sfds"), - hint = "Okay", - onValueChanged = {} - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyBorderButton.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyBorderButton.kt deleted file mode 100644 index d44b005bfe..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyBorderButton.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.SolidColor -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gradient - - -@Composable -fun IvyBorderButton( - modifier: Modifier = Modifier, - text: String, - textStyle: TextStyle = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ), - backgroundGradient: Gradient = Gradient.solid(UI.colorsInverted.medium), - @DrawableRes iconStart: Int? = null, - @DrawableRes iconEnd: Int? = null, - iconTint: Color = UI.colorsInverted.pure, - enabled: Boolean = true, - wrapContentMode: Boolean = true, - - padding: Dp = 12.dp, - onClick: () -> Unit -) { - Row( - modifier = modifier - .clip(UI.shapes.fullyRounded) - .border( - width = 2.dp, - brush = if (enabled) - backgroundGradient.asHorizontalBrush() else SolidColor(UI.colors.neutral), - shape = UI.shapes.fullyRounded - ) - .clickable(onClick = onClick, enabled = enabled), - verticalAlignment = Alignment.CenterVertically - ) { - - when { - iconStart != null -> { - IconStart( - icon = iconStart, - tint = iconTint, - ) - } - iconEnd != null && !wrapContentMode -> { - IconEnd( - icon = iconEnd, - tint = Color.Transparent - ) - } - else -> { - Spacer(modifier = Modifier.width(24.dp)) - } - } - - if (!wrapContentMode) { - Spacer(modifier = Modifier.weight(1f)) - } - - Text( - modifier = Modifier.padding( - vertical = padding, - ), - text = text, - style = textStyle - ) - - if (!wrapContentMode) { - Spacer(modifier = Modifier.weight(1f)) - } - - when { - iconStart != null && !wrapContentMode -> { - IconStart( - icon = iconStart, - tint = Color.Transparent, - ) - } - iconEnd != null -> { - IconEnd( - icon = iconEnd, - tint = iconTint, - ) - } - else -> { - Spacer(modifier = Modifier.width(24.dp)) - } - } - } -} - -@Composable -private fun IconStart( - icon: Int, - tint: Color, -) { - Spacer(modifier = Modifier.width(12.dp)) - - Icon( - modifier = Modifier, - painter = painterResource(id = icon), - contentDescription = "icon", - tint = tint, - ) - - Spacer(modifier = Modifier.width(4.dp)) -} - -@Composable -private fun IconEnd( - icon: Int, - tint: Color, -) { - Spacer(modifier = Modifier.width(4.dp)) - - Icon( - modifier = Modifier, - painter = painterResource(id = icon), - contentDescription = "icon", - tint = tint, - ) - - Spacer(modifier = Modifier.width(12.dp)) -} - -@Preview -@Composable -private fun PreviewIvyBorderButton() { - ComponentPreview { - IvyBorderButton( - text = "New label", - iconStart = R.drawable.ic_label_hashtag - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyButton.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyButton.kt deleted file mode 100644 index 5b7ca65b4b..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyButton.kt +++ /dev/null @@ -1,243 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.SolidColor -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.thenIf - -@Composable -fun IvyButton( - modifier: Modifier = Modifier, - text: String, - backgroundGradient: Gradient = GradientIvy, - textStyle: TextStyle = UI.typo.b2.style( - color = White, - fontWeight = FontWeight.Bold - ), - @DrawableRes iconStart: Int? = null, - @DrawableRes iconEnd: Int? = null, - iconTint: Color = White, - enabled: Boolean = true, - shadowAlpha: Float = 0.15f, - wrapContentMode: Boolean = true, - hasGlow: Boolean = true, - padding: Dp = 12.dp, - iconEdgePadding: Dp = 12.dp, - iconTextPadding: Dp = 4.dp, - onClick: () -> Unit -) { - Row( - modifier = modifier - .thenIf(enabled && hasGlow) { - drawColoredShadow( - color = backgroundGradient.startColor, - borderRadius = 0.dp, - shadowRadius = 16.dp, - alpha = shadowAlpha, - offsetX = 0.dp, - offsetY = 8.dp - ) - } - .clip(UI.shapes.fullyRounded) - .background( - brush = if (enabled) - backgroundGradient.asHorizontalBrush() else SolidColor(UI.colors.neutral), - shape = UI.shapes.fullyRounded - ) - .clickable(onClick = onClick, enabled = enabled), - verticalAlignment = Alignment.CenterVertically - ) { - - when { - iconStart != null -> { - IconStart( - icon = iconStart, - tint = iconTint, - iconEdgePadding = iconEdgePadding, - iconTextPadding = iconTextPadding - ) - } - iconEnd != null && !wrapContentMode -> { - IconEnd( - icon = iconEnd, - tint = Color.Transparent, - iconEdgePadding = iconEdgePadding, - iconTextPadding = iconTextPadding - ) - } - else -> { - Spacer(modifier = Modifier.width(24.dp)) - } - } - - if (!wrapContentMode) { - Spacer(modifier = Modifier.weight(1f)) - } - - Text( - modifier = Modifier.padding( - vertical = padding, - ), - text = text, - style = textStyle - ) - - if (!wrapContentMode) { - Spacer(modifier = Modifier.weight(1f)) - } - - when { - iconStart != null && !wrapContentMode -> { - IconStart( - icon = iconStart, - tint = Color.Transparent, - iconEdgePadding = iconEdgePadding, - iconTextPadding = iconTextPadding - ) - } - iconEnd != null -> { - IconEnd( - icon = iconEnd, - tint = iconTint, - iconEdgePadding = iconEdgePadding, - iconTextPadding = iconTextPadding - ) - } - else -> { - Spacer(modifier = Modifier.width(24.dp)) - } - } - } -} - -@Composable -private fun IconStart( - iconEdgePadding: Dp, - iconTextPadding: Dp, - icon: Int, - tint: Color, -) { - Spacer(modifier = Modifier.width(iconEdgePadding)) - - Icon( - modifier = Modifier, - painter = painterResource(id = icon), - contentDescription = "icon", - tint = tint, - ) - - Spacer(modifier = Modifier.width(iconTextPadding)) -} - -@Composable -private fun IconEnd( - iconEdgePadding: Dp, - iconTextPadding: Dp, - icon: Int, - tint: Color, -) { - Spacer(modifier = Modifier.width(iconTextPadding)) - - Icon( - modifier = Modifier, - painter = painterResource(id = icon), - contentDescription = "icon", - tint = tint, - ) - - Spacer(modifier = Modifier.width(iconEdgePadding)) -} - -@Preview -@Composable -private fun PreviewIvyButtonWrapContentWithIconStart() { - ComponentPreview { - IvyButton( - modifier = Modifier - .padding(horizontal = 24.dp) - .wrapContentSize(), - iconStart = R.drawable.ic_plus, - text = "Add new", - wrapContentMode = true - ) { - - } - } -} - -@Preview -@Composable -private fun PreviewIvyButtonFillMaxWidthWithIconStart() { - ComponentPreview { - IvyButton( - modifier = Modifier - .padding(horizontal = 24.dp) - .fillMaxWidth(), - iconStart = R.drawable.ic_plus, - text = "Add new", - wrapContentMode = false - ) { - - } - } -} - -@Preview -@Composable -private fun PreviewIvyButtonWrapContentWithIconEnd() { - ComponentPreview { - IvyButton( - modifier = Modifier - .padding(horizontal = 24.dp) - .wrapContentSize(), - backgroundGradient = Gradient(Ivy, Ivy), - iconEnd = R.drawable.ic_onboarding_next_arrow, - text = "Category 1", - wrapContentMode = true - ) { - - } - } -} - -@Preview -@Composable -private fun PreviewIvyButtonFillMaxWidthWithIconEnd() { - ComponentPreview { - IvyButton( - modifier = Modifier - .padding(horizontal = 24.dp) - .fillMaxWidth(), - backgroundGradient = Gradient(Ivy, Ivy), - iconEnd = R.drawable.ic_onboarding_next_arrow, - text = "Category 1", - wrapContentMode = false - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyCheckbox.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyCheckbox.kt deleted file mode 100644 index 5e946814f0..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyCheckbox.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.clickableNoIndication - - -@Composable -fun IvyCheckbox( - modifier: Modifier = Modifier, - checked: Boolean, - onCheckedChange: (checked: Boolean) -> Unit -) { - Icon( - modifier = modifier - .size(48.dp) - .clip(CircleShape) - .clickable(onClick = { - onCheckedChange(!checked) - }) - .padding(all = 12.dp), - - painter = painterResource( - id = if (checked) R.drawable.ic_checkbox_checked else R.drawable.ic_checkbox_unchecked - ), - contentDescription = null, - tint = if (checked) Color.Unspecified else UI.colors.neutral - ) -} - -@Composable -fun IvyCheckboxWithText( - modifier: Modifier = Modifier, - text: String, - checked: Boolean, - onCheckedChange: (checked: Boolean) -> Unit -) { - Row( - modifier = modifier - .clickableNoIndication { - onCheckedChange(!checked) - }, - verticalAlignment = Alignment.CenterVertically - ) { - IvyCheckbox( - checked = checked, - onCheckedChange = onCheckedChange - ) - - Spacer(modifier = Modifier.width(4.dp)) - - Text( - text = text, - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.SemiBold - ) - ) - } -} - -@Preview -@Composable -private fun PreviewIvyCheckboxWithText() { - ComponentPreview { - IvyCheckboxWithText( - text = "Default category", - checked = false, - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyChecklistTextField.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyChecklistTextField.kt deleted file mode 100644 index 4e30613151..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyChecklistTextField.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.* -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.clickableNoIndication -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank - - -@Composable -fun IvyChecklistTextField( - modifier: Modifier = Modifier, - textModifier: Modifier = Modifier, - value: TextFieldValue, - hint: String?, - readOnly: Boolean = false, - fontWeight: FontWeight = FontWeight.Medium, - hintFontWeight: FontWeight = FontWeight.Medium, - textColor: Color = UI.colorsInverted.pure, - hintColor: Color = UI.colorsInverted.medium, - textAlign: TextAlign = TextAlign.Start, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions? = KeyboardOptions.Default, - keyboardActions: KeyboardActions? = KeyboardActions.Default, - paddingVertical: Dp = 16.dp, - onValueChanged: (TextFieldValue) -> Unit -) { - val isEmpty = value.text.isBlank() - - Box( - modifier = modifier, - contentAlignment = when (textAlign) { - TextAlign.Left -> Alignment.CenterStart - TextAlign.Right -> Alignment.CenterEnd - TextAlign.Center -> Alignment.Center - TextAlign.Justify -> Alignment.CenterEnd - TextAlign.Start -> Alignment.CenterStart - TextAlign.End -> Alignment.CenterEnd - else -> Alignment.CenterEnd - } - ) { - val inputFieldFocus = FocusRequester() - - if (isEmpty && hint.isNotNullOrBlank()) { - Text( - modifier = textModifier - .clickableNoIndication { - inputFieldFocus.requestFocus() - } - .padding(vertical = paddingVertical), - text = hint!!, - textAlign = textAlign, - style = UI.typo.b2.style( - color = hintColor, - fontWeight = hintFontWeight, - textAlign = textAlign - ) - ) - } - - val view = LocalView.current - BasicTextField( - modifier = textModifier - .focusRequester(inputFieldFocus) - .clickableNoIndication { - inputFieldFocus.requestFocus() - } - .padding(vertical = paddingVertical), - value = value, - onValueChange = onValueChanged, - readOnly = readOnly, - textStyle = UI.typo.b2.style( - color = textColor, - fontWeight = fontWeight, - textAlign = textAlign - ), - singleLine = false, - cursorBrush = SolidColor(UI.colorsInverted.pure), - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions ?: KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done - ), - keyboardActions = keyboardActions ?: KeyboardActions( - onDone = { - hideKeyboard(view) - } - ) - ) - } -} - - -@Preview -@Composable -private fun PreviewIvyTextField() { - ComponentPreview { - IvyChecklistTextField( - modifier = Modifier - .align(Alignment.CenterStart) - .background(UI.colors.red) - .padding(horizontal = 24.dp), - value = TextFieldValue(), - hint = "Hint", - onValueChanged = {}) - } -} - -@Preview -@Composable -private fun PreviewIvyTextField_longText() { - ComponentPreview { - IvyChecklistTextField( - modifier = Modifier - .background(UI.colors.red) - .padding(horizontal = 24.dp), - value = TextFieldValue("Cur habitio favere? Sunt navises promissio grandis, primus accolaes. Yes, there is chaos, it contacts with light."), - hint = "Hint", - onValueChanged = {}) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyCircleButton.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyCircleButton.kt deleted file mode 100644 index 1d3c8413c0..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyCircleButton.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.GradientRed -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.thenIf - - -@Composable -fun IvyCircleButton( - modifier: Modifier = Modifier, - backgroundPadding: Dp = 0.dp, - backgroundGradient: Gradient = GradientIvy, - horizontalGradient: Boolean = true, - @DrawableRes icon: Int, - tint: Color = White, - enabled: Boolean = true, - hasShadow: Boolean = true, - onClick: () -> Unit -) { - IvyIcon( - modifier = modifier - .thenIf(enabled && hasShadow) { - drawColoredShadow( - color = backgroundGradient.startColor, - borderRadius = 0.dp, - shadowRadius = 16.dp, - offsetX = 0.dp, - offsetY = 8.dp - ) - } - .clip(UI.shapes.fullyRounded) - .background( - brush = if (enabled) { - if (horizontalGradient) - backgroundGradient.asHorizontalBrush() else backgroundGradient.asVerticalBrush() - } else { - SolidColor(UI.colors.neutral) - }, - shape = UI.shapes.fullyRounded - ) - .clickable(onClick = onClick, enabled = enabled) - .padding(all = backgroundPadding), - icon = icon, - tint = tint, - contentDescription = "circle button" - ) -} - -@Preview -@Composable -private fun PreviewIvyCircleButton_Enabled() { - ComponentPreview { - IvyCircleButton( - icon = R.drawable.ic_delete, - backgroundGradient = GradientRed, - tint = White - ) { - - } - } -} - -@Preview -@Composable -private fun PreviewIvyCircleButton_Disabled() { - ComponentPreview { - IvyCircleButton( - icon = R.drawable.ic_delete, - backgroundGradient = GradientRed, - enabled = false, - tint = White - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyColorPicker.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyColorPicker.kt deleted file mode 100644 index 22a31e76ae..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyColorPicker.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -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.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.IVY_COLOR_PICKER_COLORS_FREE -import com.ivy.base.IVY_COLOR_PICKER_COLORS_PREMIUM -import com.ivy.design.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.dynamicContrast -import com.ivy.wallet.utils.densityScope -import com.ivy.wallet.utils.thenIf - -private data class IvyColor( - val color: Color, - val premium: Boolean -) - -@Composable -fun ColumnScope.IvyColorPicker( - selectedColor: Color, - onColorSelected: (Color) -> Unit -) { - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = "Choose color", - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - val freeIvyColors = IVY_COLOR_PICKER_COLORS_FREE - .map { - IvyColor( - color = it, - premium = false - ) - } - - val premiumIvyColors = IVY_COLOR_PICKER_COLORS_PREMIUM - .map { - IvyColor( - color = it, - premium = true - ) - } - - val ivyColors = freeIvyColors + premiumIvyColors - - - val listState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() - - densityScope { - - } - - LazyRow( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment= Alignment.CenterVertically, - state = listState - ) { - items( - count = ivyColors.size - ) { index -> - ColorItem( - index = index, - ivyColor = ivyColors[index], - selectedColor = selectedColor, - onSelected = { - if (it.premium) { - onColorSelected(it.color) - } else { - onColorSelected(it.color) - } - } - ) - } - } -} - -@Composable -private fun ColorItem( - index: Int, - ivyColor: IvyColor, - selectedColor: Color, - onSelected: (IvyColor) -> Unit -) { - val color = ivyColor.color - val selected = color == selectedColor - - if (index == 0) { - Spacer(Modifier.width(24.dp)) - } - - Box( - modifier = Modifier - .clip(CircleShape) - .size(48.dp) - .background(color, CircleShape) - .thenIf(selected) { - border(width = 4.dp, color = color.dynamicContrast(), CircleShape) - } - .clickable(onClick = { - onSelected(ivyColor) - }) - .testTag("color_item_${ivyColor.color.value}"), - contentAlignment = Alignment.Center - ) { - if (ivyColor.premium && false) { - IvyIcon( - icon = R.drawable.ic_custom_safe_s, - tint = color.dynamicContrast() - ) - } - } - - Spacer(Modifier.width(if (selected) 16.dp else 24.dp)) -} - -@Preview -@Composable -private fun PreviewIvyColorPicker() { - ComponentPreview { - Column { - IvyColorPicker(selectedColor = UI.colors.primary) { - - } - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyComponents.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyComponents.kt deleted file mode 100644 index 1cf18dbc98..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyComponents.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI - - -@Composable -fun ActionsRow( - modifier: Modifier = Modifier, - lineColor: Color = UI.colors.medium, - Content: @Composable RowScope.() -> Unit -) { - Row( - modifier = modifier - .fillMaxWidth() - .drawBehind { - val height = this.size.height - val width = this.size.width - - drawLine( - color = lineColor, - strokeWidth = 2.dp.toPx(), - start = Offset( - x = 0f, - y = height / 2 - ), - end = Offset( - x = width, - y = height / 2 - ) - ) - }, - verticalAlignment = Alignment.CenterVertically - ) { - Content() - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyDescriptionTextField.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyDescriptionTextField.kt deleted file mode 100644 index 8f74b64ee7..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyDescriptionTextField.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.* -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank - - -@Composable -fun IvyDescriptionTextField( - modifier: Modifier = Modifier, - textModifier: Modifier = Modifier, - testTag: String = "desc_input", - value: TextFieldValue, - hint: String?, - fontWeight: FontWeight = FontWeight.Medium, - textColor: Color = UI.colorsInverted.pure, - hintColor: Color = UI.colorsInverted.medium, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions? = KeyboardOptions.Default, - keyboardActions: KeyboardActions? = KeyboardActions.Default, - onValueChanged: (TextFieldValue) -> Unit -) { - val isEmpty = value.text.isBlank() - - Box( - modifier = modifier, - contentAlignment = Alignment.TopStart - ) { - if (isEmpty && hint.isNotNullOrBlank()) { - Text( - modifier = textModifier, - text = hint!!, - textAlign = TextAlign.Start, - style = UI.typo.b2.style( - color = hintColor, - fontWeight = fontWeight, - textAlign = TextAlign.Start - ) - ) - } - - val view = LocalView.current - BasicTextField( - modifier = textModifier.testTag(testTag), - value = value, - onValueChange = onValueChanged, - textStyle = UI.typoSecond.b2.style( - color = textColor, - fontWeight = fontWeight, - textAlign = TextAlign.Start - ), - singleLine = false, - cursorBrush = SolidColor(UI.colorsInverted.pure), - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions ?: KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done - ), - keyboardActions = keyboardActions ?: KeyboardActions( - onDone = { - hideKeyboard(view) - } - ) - ) - } -} - - -@Preview -@Composable -private fun PreviewIvyTextField() { - ComponentPreview { - IvyDescriptionTextField( - modifier = Modifier - .align(Alignment.CenterStart) - .background(UI.colors.red) - .padding(horizontal = 24.dp), - value = TextFieldValue(), - hint = "Hint", - onValueChanged = {}) - } -} - -@Preview -@Composable -private fun PreviewIvyTextField_longText() { - ComponentPreview { - IvyDescriptionTextField( - modifier = Modifier - .background(UI.colors.red) - .padding(horizontal = 24.dp), - value = TextFieldValue("Cur habitio favere? Sunt navises promissio grandis, primus accolaes. Yes, there is chaos, it contacts with light."), - hint = "Hint", - onValueChanged = {}) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyDivider.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyDivider.kt deleted file mode 100644 index 69b1ec9895..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyDivider.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview - - -@Composable -fun IvyDividerLine( - modifier: Modifier = Modifier -) { - Spacer( - modifier = modifier - .fillMaxWidth() - .height(2.dp) - .background(UI.colors.medium) - ) -} - -@Composable -fun IvyDividerLineRounded( - modifier: Modifier = Modifier -) { - Spacer( - modifier = modifier - .fillMaxWidth() - .height(2.dp) - .background(UI.colors.medium, UI.shapes.fullyRounded) - ) -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - IvyDividerLine() - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyIcon.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyIcon.kt deleted file mode 100644 index 14af7b3f3c..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyIcon.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.material.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import com.ivy.design.l0_system.UI - - -@Composable -fun IvyIcon( - modifier: Modifier = Modifier, - @DrawableRes icon: Int, - tint: Color = UI.colorsInverted.pure, - contentDescription: String = "icon" -) { - Icon( - modifier = modifier, - painter = painterResource(id = icon), - contentDescription = contentDescription, - tint = tint - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyNameTextFieldValue.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyNameTextFieldValue.kt deleted file mode 100644 index 542cfa988c..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyNameTextFieldValue.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.* -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank - - -@Composable -fun IvyNameTextField( - modifier: Modifier = Modifier, - underlineModifier: Modifier = Modifier, - value: TextFieldValue, - textColor: Color = UI.colorsInverted.pure, - hint: String?, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done, - capitalization = KeyboardCapitalization.Sentences - ), - keyboardActions: KeyboardActions? = null, - onValueChanged: (TextFieldValue) -> Unit -) { - Column { - val isEmpty = value.text.isBlank() - - Box( - modifier = modifier, - contentAlignment = Alignment.CenterStart - ) { - if (isEmpty && hint.isNotNullOrBlank()) { - Text( - modifier = Modifier, - text = hint!!, - style = UI.typo.b2.style( - color = UI.colors.neutral, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Start - ), - ) - } - - val view = LocalView.current - BasicTextField( - modifier = Modifier - .testTag("base_input"), - value = value, - onValueChange = onValueChanged, - textStyle = UI.typo.b1.style( - color = textColor, - fontWeight = FontWeight.ExtraBold, - textAlign = TextAlign.Start - ), - singleLine = false, - cursorBrush = SolidColor(UI.colorsInverted.pure), - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions ?: KeyboardActions( - onDone = { - hideKeyboard(view) - } - ) - ) - } - - Spacer(Modifier.height(8.dp)) - - IvyDividerLineRounded( - modifier = underlineModifier - ) - } -} - - -@Preview -@Composable -private fun PreviewIvyNameTextField() { - ComponentPreview { - Column( - verticalArrangement = Arrangement.Center - ) { - IvyNameTextField( - modifier = Modifier.padding(horizontal = 32.dp), - underlineModifier = Modifier.padding(horizontal = 24.dp), - value = TextFieldValue("Title"), - hint = "Title", - onValueChanged = {} - ) - } - - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyNumberTextField.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyNumberTextField.kt deleted file mode 100644 index 57418533bd..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyNumberTextField.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.* -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank - - -@Composable -fun IvyNumberTextField( - modifier: Modifier = Modifier, - textModifier: Modifier = Modifier, - value: TextFieldValue, - hint: String?, - fontWeight: FontWeight = FontWeight.ExtraBold, - textColor: Color = UI.colorsInverted.pure, - hintColor: Color = Color.Gray, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions? = null, - keyboardActions: KeyboardActions? = null, - onValueChanged: (TextFieldValue) -> Unit -) { - val isEmpty = value.text.isBlank() - - Box( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - if (isEmpty && hint.isNotNullOrBlank()) { - Text( - modifier = textModifier, - text = hint!!, - textAlign = TextAlign.Start, - style = UI.typoSecond.b2.style( - color = hintColor, - fontWeight = fontWeight, - textAlign = TextAlign.Center - ) - ) - } - - val view = LocalView.current - BasicTextField( - modifier = textModifier - .testTag("base_number_input"), - value = value, - onValueChange = onValueChanged, - textStyle = UI.typoSecond.b2.style( - color = textColor, - fontWeight = fontWeight, - textAlign = TextAlign.Center - ), - singleLine = true, - cursorBrush = SolidColor(UI.colorsInverted.pure), - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions ?: KeyboardOptions( - capitalization = KeyboardCapitalization.Characters, - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number, - autoCorrect = false - ), - keyboardActions = keyboardActions ?: KeyboardActions( - onDone = { - hideKeyboard(view) - } - ) - ) - } -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - IvyNumberTextField( - value = TextFieldValue(), - hint = "0" - ) { - - } - } -} diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyOutlinedButton.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyOutlinedButton.kt deleted file mode 100644 index 9eea165c44..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyOutlinedButton.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.utils.thenIf - - -@Composable -fun IvyOutlinedButton( - modifier: Modifier = Modifier, - text: String, - @DrawableRes iconStart: Int?, - solidBackground: Boolean = false, - iconTint: Color = UI.colorsInverted.pure, - borderColor: Color = UI.colors.medium, - textColor: Color = UI.colorsInverted.pure, - padding: Dp = 12.dp, - onClick: () -> Unit, -) { - Row( - modifier = modifier - .clip(UI.shapes.fullyRounded) - .clickable( - onClick = onClick - ) - .border(2.dp, borderColor, UI.shapes.fullyRounded) - .thenIf(solidBackground) { - background(UI.colors.pure, UI.shapes.fullyRounded) - }, - verticalAlignment = Alignment.CenterVertically - ) { - if (iconStart != null) { - Spacer(Modifier.width(12.dp)) - - IvyIcon( - icon = iconStart, - tint = iconTint - ) - - Spacer(Modifier.width(4.dp)) - } else { - Spacer(Modifier.width(24.dp)) - } - - Text( - modifier = Modifier.padding(vertical = padding), - text = text, - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - color = textColor - ) - ) - - Spacer(Modifier.width(24.dp)) - } -} - -@Composable -fun IvyOutlinedButtonFillMaxWidth( - modifier: Modifier = Modifier, - text: String, - @DrawableRes iconStart: Int?, - solidBackground: Boolean = false, - iconTint: Color = UI.colorsInverted.pure, - borderColor: Color = UI.colors.medium, - textColor: Color = UI.colorsInverted.pure, - padding: Dp = 16.dp, - onClick: () -> Unit, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(UI.shapes.fullyRounded) - .clickable( - onClick = onClick - ) - .border(2.dp, borderColor, UI.shapes.fullyRounded) - .thenIf(solidBackground) { - background(UI.colors.pure, UI.shapes.fullyRounded) - }, - verticalAlignment = Alignment.CenterVertically - ) { - if (iconStart != null) { - Spacer(Modifier.width(12.dp)) - - IvyIcon( - icon = iconStart, - tint = iconTint - ) - } - - Spacer(Modifier.weight(1f)) - - Text( - modifier = Modifier.padding(vertical = padding), - text = text, - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - color = textColor - ) - ) - - Spacer(Modifier.weight(1f)) - - if (iconStart != null) { - Spacer(Modifier.width(12.dp)) - - IvyIcon( - icon = iconStart, - tint = Color.Transparent, - ) - } - } -} - -@Preview -@Composable -private fun Preview_FillMaxWidth() { - ComponentPreview { - IvyOutlinedButtonFillMaxWidth( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Import backup file", - iconStart = R.drawable.ic_export_csv, - textColor = Green, - iconTint = Green - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyOutlinedTextField.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyOutlinedTextField.kt deleted file mode 100644 index 4bc81c4c0f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyOutlinedTextField.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.isNotNullOrBlank -import com.ivy.wallet.utils.thenIf - - -@Composable -fun IvyOutlinedTextField( - modifier: Modifier = Modifier, - value: TextFieldValue, - hint: String?, - hintColor: Color = UI.colors.neutral, - backgroundColor: Color = UI.colors.primary, - emptyBorderColor: Color = UI.colors.neutral, - textColor: Color = UI.colorsInverted.pure, - cursorColor: Color = UI.colorsInverted.pure, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - validateInput: (TextFieldValue) -> Boolean = { it.text.isNotNullOrBlank() }, - onValueChanged: (TextFieldValue) -> Unit -) { - val isEmpty = value.text.isBlank() - - Box( - modifier = modifier - .clip(UI.shapes.fullyRounded) - .border( - width = 2.dp, - color = if (isEmpty) emptyBorderColor else backgroundColor, - shape = UI.shapes.fullyRounded - ) - .thenIf(validateInput(value)) { - background(backgroundColor.copy(alpha = 0.1f), UI.shapes.fullyRounded) - }, - contentAlignment = Alignment.Center - ) { - val inputFieldFocus = FocusRequester() - - if (isEmpty && hint.isNotNullOrBlank()) { - Text( - modifier = Modifier - .clickable { - inputFieldFocus.requestFocus() - } - .fillMaxWidth() - .padding(vertical = 16.dp), - text = hint!!, - textAlign = TextAlign.Center, - style = UI.typo.b2.style( - color = hintColor, - fontWeight = FontWeight.SemiBold - ) - ) - } - - BasicTextField( - modifier = Modifier - .focusRequester(inputFieldFocus) - .clickable { - inputFieldFocus.requestFocus() - } - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 24.dp), - value = value, - onValueChange = onValueChanged, - textStyle = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ), - singleLine = true, - cursorBrush = SolidColor(cursorColor), - visualTransformation = visualTransformation, - keyboardActions = keyboardActions, - keyboardOptions = keyboardOptions - ) - } -} - - -@Preview -@Composable -private fun PreviewOutlineTextField() { - ComponentPreview { - IvyOutlinedTextField( - modifier = Modifier.padding(horizontal = 24.dp), - value = TextFieldValue(), - hint = "Hint", - onValueChanged = {}) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvySwitch.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvySwitch.kt deleted file mode 100644 index 8bb02490a3..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvySwitch.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.utils.springBounce - - -@Composable -fun IvySwitch( - modifier: Modifier = Modifier, - enabled: Boolean, - onEnabledChange: (checked: Boolean) -> Unit -) { - val color by animateColorAsState( - targetValue = if (enabled) Green else Gray, - animationSpec = springBounce() - ) - - Row( - modifier = modifier - .width(40.dp) - .clip(UI.shapes.fullyRounded) - .border(2.dp, color, UI.shapes.fullyRounded) - .clickable { - onEnabledChange(!enabled) - } - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val weightStart by animateFloatAsState( - targetValue = if (enabled) 1f else 0f, - animationSpec = springBounce() - ) - - Spacer(Modifier.width(4.dp)) - - if (weightStart > 0) { - Spacer(Modifier.weight(weightStart)) - } - - //Circle - Spacer( - modifier = Modifier - .size(16.dp) - .background(color, CircleShape) - ) - - val weightEnd = 1f - weightStart - if (weightEnd > 0) { - Spacer(Modifier.weight(weightEnd)) - } - - Spacer(Modifier.width(4.dp)) - } -} - -@Preview -@Composable -private fun PreviewIvySwitch() { - ComponentPreview { - var enabled by remember { - mutableStateOf(false) - } - - IvySwitch(enabled = enabled) { - enabled = it - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyTitleTextField.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyTitleTextField.kt deleted file mode 100644 index 72b84d14e6..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyTitleTextField.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.* -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank - - -@Composable -fun ColumnScope.IvyTitleTextField( - modifier: Modifier = Modifier, - dividerModifier: Modifier = Modifier, - value: TextFieldValue, - textColor: Color = UI.colorsInverted.pure, - hint: String?, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done, - capitalization = KeyboardCapitalization.Sentences - ), - keyboardActions: KeyboardActions? = null, - onValueChanged: (TextFieldValue) -> Unit -) { - val isEmpty = value.text.isBlank() - - Box( - modifier = modifier, - contentAlignment = Alignment.CenterStart - ) { - if (isEmpty && hint.isNotNullOrBlank()) { - Text( - modifier = Modifier, - text = hint!!, - style = UI.typo.h2.style( - color = UI.colors.neutral, - fontWeight = FontWeight.Black, - textAlign = TextAlign.Start - ), - ) - } - - val view = LocalView.current - BasicTextField( - modifier = Modifier - .testTag("input_field"), - value = value, - onValueChange = onValueChanged, - textStyle = UI.typo.h2.style( - color = textColor, - fontWeight = FontWeight.Black, - textAlign = TextAlign.Start - ), - singleLine = false, - cursorBrush = SolidColor(UI.colorsInverted.pure), - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions ?: KeyboardActions( - onDone = { - hideKeyboard(view) - } - ) - ) - } - - Spacer(Modifier.height(8.dp)) - - Spacer( - modifier = dividerModifier - .fillMaxWidth() - .height(2.dp) - .background(UI.colors.medium, UI.shapes.fullyRounded), - ) -} - - -@Preview -@Composable -private fun PreviewIvyTitleTextField() { - ComponentPreview { - Column( - verticalArrangement = Arrangement.Center - ) { - IvyTitleTextField( - modifier = Modifier.padding(horizontal = 32.dp), - dividerModifier = Modifier.padding(horizontal = 24.dp), - value = TextFieldValue("Title"), - hint = "Title", - onValueChanged = {} - ) - } - - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyToolbar.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyToolbar.kt deleted file mode 100644 index c547ec8d80..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/IvyToolbar.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.wallet.ui.theme.gradientCutBackgroundBottom - -enum class BackButtonType { - BACK, CLOSE -} - -@Composable -fun IvyToolbar( - onBack: () -> Unit, - backButtonType: BackButtonType = BackButtonType.BACK, - paddingTop: Dp = 16.dp, - paddingBottom: Dp = 16.dp, - Content: @Composable RowScope.() -> Unit = { } -) { - Row( - modifier = Modifier - .fillMaxWidth() - .gradientCutBackgroundBottom(paddingBottom = paddingBottom) - .padding(top = paddingTop), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - when (backButtonType) { - BackButtonType.BACK -> { - BackButton( - modifier = Modifier.testTag("toolbar_back") - ) { - onBack() - } - } - BackButtonType.CLOSE -> { - CloseButton( - modifier = Modifier.testTag("toolbar_close") - ) { - onBack() - } - } - } - - Content() - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/OnboardingComponents.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/OnboardingComponents.kt deleted file mode 100644 index 9dfbcc4165..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/OnboardingComponents.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -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.SolidColor -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.thenIf - - -@Composable -fun OnboardingButton( - modifier: Modifier = Modifier, - text: String, - textColor: Color, - backgroundGradient: Gradient, - @DrawableRes iconStart: Int? = null, - hasNext: Boolean = false, - enabled: Boolean = true, - onClick: () -> Unit -) { - Box( - modifier = modifier - .thenIf(enabled) { - drawColoredShadow( - color = backgroundGradient.startColor, - borderRadius = 0.dp, - shadowRadius = 16.dp, - offsetX = 0.dp, - offsetY = 8.dp - ) - } - .clip(UI.shapes.fullyRounded) - .background( - brush = if (enabled) - backgroundGradient.asHorizontalBrush() else SolidColor(UI.colors.neutral), - shape = UI.shapes.fullyRounded - ) - .clickable(onClick = onClick, enabled = enabled), - contentAlignment = Alignment.Center - ) { - if (iconStart != null) { - Image( - modifier = Modifier - .align(Alignment.CenterStart) - .padding(vertical = 8.dp) - .padding(start = 24.dp), - painter = painterResource(id = iconStart), - contentDescription = "iconStart" - ) - } - - Text( - modifier = Modifier.padding(vertical = 16.dp), - text = text, - style = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.Bold - ) - ) - - if (hasNext && enabled) { - Image( - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(vertical = 8.dp) - .padding(end = 24.dp), - painter = painterResource(id = R.drawable.ic_onboarding_next_arrow), - contentDescription = "next" - ) - } - } -} - -@Preview -@Composable -private fun PreviewOnboardingTextField() { - ComponentPreview { - IvyOutlinedTextField( - modifier = Modifier.padding(horizontal = 24.dp), - value = TextFieldValue("iliyan.germanov971@gmail.com"), - hint = "Enter email", - onValueChanged = {} - ) - } -} - -@Preview -@Composable -private fun PreviewOnboardingButton() { - ComponentPreview { - OnboardingButton( - modifier = Modifier - .padding(horizontal = 24.dp) - .fillMaxWidth(), - text = "Login", - backgroundGradient = GradientIvy, - hasNext = true, - textColor = UI.colors.pure, - iconStart = null, - enabled = false, - onClick = { } - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/ProgressBar.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/ProgressBar.kt deleted file mode 100644 index ed65f9936e..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/ProgressBar.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -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.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.* - - -@Composable -fun ProgressBar( - modifier: Modifier = Modifier, - notFilledColor: Color = UI.colors.pure, - positiveProgress: Boolean = true, - percent: Double -) { - Spacer( - modifier = modifier - .clip(UI.shapes.squared) - .background(notFilledColor) - .drawBehind { - drawRect( - color = when { - percent <= 0.25 -> { - if (positiveProgress) Red else Green - } - percent <= 0.50 -> { - if (positiveProgress) Orange else Ivy - } - percent <= 0.75 -> { - if (positiveProgress) Ivy else Orange - } - else -> if (positiveProgress) Green else Red - }, - size = size.copy( - width = (size.width * percent).toFloat() - ) - ) - }, - ) -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - ProgressBar( - modifier = Modifier - .fillMaxWidth() - .height(24.dp) - .padding(horizontal = 16.dp), - notFilledColor = Gray, - percent = 0.6 - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/ReorderModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/ReorderModal.kt deleted file mode 100644 index ac38b467b0..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/ReorderModal.kt +++ /dev/null @@ -1,388 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import android.annotation.SuppressLint -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.ItemTouchHelper.* -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.ivy.base.R -import com.ivy.base.Reorderable -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.GradientGreen -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.utils.numberBetween -import com.ivy.wallet.utils.swap -import java.util.* - -@Suppress("UNCHECKED_CAST") -@Composable -fun BoxScope.ReorderModalSingleType( - visible: Boolean, - id: UUID = UUID.randomUUID(), - TitleContent: @Composable ColumnScope.() -> Unit = { - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.reorder), - style = UI.typo.b1.style( - UI.colorsInverted.pure, - FontWeight.ExtraBold - ) - ) - }, - initialItems: List, - dismiss: () -> Unit, - onUpdateItemOrderNum: (item: T, newOrderNum: Double) -> Unit = { _, _ -> }, - onReordered: ((List) -> Unit)? = null, - ItemContent: @Composable (Int, T) -> Unit -) { - ReorderModal( - visible = visible, - id = id, - initialItems = initialItems, - TitleContent = TitleContent, - dismiss = dismiss, - onUpdateItemOrderNum = { _, item, newOrderNum -> - onUpdateItemOrderNum(item, newOrderNum) - }, - onReordered = { listAny -> - onReordered?.invoke( - listAny as? List ?: error("List cast exception.") - ) - }, - ItemContent = { index, itemAny -> - ItemContent(index, itemAny as T) - } - ) -} - -@Composable -fun BoxScope.ReorderModal( - visible: Boolean, - id: UUID = UUID.randomUUID(), - TitleContent: @Composable ColumnScope.() -> Unit = { - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.reorder), - style = UI.typo.b1.style( - UI.colorsInverted.pure, - FontWeight.ExtraBold - ) - ) - }, - initialItems: List, - dismiss: () -> Unit, - onUpdateItemOrderNum: ( - itemsInNewOrder: List, - item: T, - newOrderNum: Double - ) -> Unit = { _, _, _ -> }, - onReordered: ((List) -> Unit)? = null, - ItemContent: @Composable RowScope.(Int, Any) -> Unit -) { - var items by remember(id, initialItems) { mutableStateOf(initialItems) } - var reOrderedList: List? by remember { - mutableStateOf(null) - } - var orderNumUpdates by remember { - mutableStateOf( - mapOf() - ) - } - - IvyModal( - id = id, - visible = visible, - scrollState = null, - dismiss = dismiss, - PrimaryAction = { - IvyCircleButton( - modifier = Modifier - .size(48.dp) - .testTag("reorder_done"), - backgroundGradient = GradientGreen, - icon = R.drawable.ic_check, - tint = White - ) { - orderNumUpdates.forEach { (item, newOrderNum) -> - onUpdateItemOrderNum(items, item, newOrderNum) - } - - onReordered?.invoke(reOrderedList ?: items) - dismiss() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - TitleContent() - - Spacer(Modifier.height(24.dp)) - - val colorMedium = UI.colors.medium - AndroidView( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - factory = { - RecyclerView(it).apply { - val itemTouchHelper = itemTouchHelper( - colorMedium = colorMedium - ) - adapter = Adapter( - itemTouchHelper = itemTouchHelper, - ItemContent = ItemContent, - addItemOrderNumUpdate = { item, newOrderNum -> - orderNumUpdates = orderNumUpdates - .toMutableMap() - .apply { - this[item] = newOrderNum - } - }, - onReorderInternalList = { reorderedItems -> - items = reorderedItems - reOrderedList = reorderedItems - } - ) - layoutManager = LinearLayoutManager(it) - itemTouchHelper.attachToRecyclerView(this) - - adapter().display(items) - } - }, - update = { - } - ) - } -} - -@Suppress("UNCHECKED_CAST") -private class Adapter( - private val itemTouchHelper: ItemTouchHelper, - private val ItemContent: @Composable RowScope.(Int, Any) -> Unit, - private val addItemOrderNumUpdate: (item: T, orderNum: Double) -> Unit, - private val onReorderInternalList: (List) -> Unit -) : RecyclerView.Adapter.ItemViewHolder>() { - val data = mutableListOf() - - @SuppressLint("NotifyDataSetChanged") - fun display(items: List) { - data.clear() - data.addAll(items) - notifyDataSetChanged() - } - - fun moveItem(from: Int, to: Int) { - data.swap(from, to) - notifyItemMoved(from, to) - } - - fun onItemMoved(item: T, to: Int) { - val newOrderNum = calculateOrderNum( - itemsInNewOrder = data, - to = to - ) - - data[to] = item.withNewOrderNum(newOrderNum) as? T - ?: error("Incorrect Reorderable implementation for $item") - addItemOrderNumUpdate(item, newOrderNum) - } - - fun onReorderInternalList() { - onReorderInternalList(data) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { - return ItemViewHolder(ComposeView(parent.context)) - } - - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - holder.display( - item = data[position], - ItemContent = ItemContent, - position = position - ) - } - - override fun getItemCount() = data.size - - inner class ItemViewHolder( - itemView: View, - ) : RecyclerView.ViewHolder(itemView) { - - fun display( - item: Any, - ItemContent: @Composable RowScope.(Int, Any) -> Unit, - position: Int - ) { - (itemView as ComposeView).setContent { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - if (item as? T != null) { - Spacer(Modifier.width(24.dp)) - - IvyIcon( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures( - onPress = { - itemTouchHelper.startDrag(this@ItemViewHolder) - } - ) - } - .testTag("reorder_drag_handle"), - icon = R.drawable.ic_drag_handle, - tint = UI.colors.neutral, - contentDescription = "reorder_${position}" - ) - - Spacer(Modifier.width(4.dp)) - } - - ItemContent(adapterPosition, item) - } - } - } - } -} - - -@Suppress("UNCHECKED_CAST") -private fun itemTouchHelper( - colorMedium: Color, -): ItemTouchHelper { - // 1. Note that I am specifying all 4 directions. - // Specifying START and END also allows - // more organic dragging than just specifying UP and DOWN. - val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { - var movedItem: T? = null - var finalTo: Int? = null - - - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter() - - val from = viewHolder.adapterPosition - val to = target.adapterPosition - - val targetItem = adapter.data[from] as? T ?: return false - - if (movedItem == null) { - movedItem = targetItem - } - finalTo = to - - adapter.moveItem(from, to) - - return true - } - - override fun onSwiped( - viewHolder: RecyclerView.ViewHolder, - direction: Int - ) { - // 4. Code block for horizontal swipe. - // ItemTouchHelper handles horizontal swipe as well, but - // it is not relevant with reordering. Ignoring here. - } - - // 1. This callback is called when a ViewHolder is selected. - // We highlight the ViewHolder here. - override fun onSelectedChanged( - viewHolder: RecyclerView.ViewHolder?, - actionState: Int - ) { - super.onSelectedChanged(viewHolder, actionState) - - if (actionState == ACTION_STATE_DRAG) { - viewHolder?.itemView?.setBackgroundColor(colorMedium.toArgb()) - } - } - - override fun clearView( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ) { - super.clearView(recyclerView, viewHolder) - viewHolder.itemView.background = null - val adapter = recyclerView.adapter() - if (movedItem != null && finalTo != null) { - adapter.onItemMoved(movedItem!!, finalTo!!) - } - adapter.onReorderInternalList() - - movedItem = null - finalTo = null - } - } - return ItemTouchHelper(simpleItemTouchCallback) -} - -@Suppress("UNCHECKED_CAST") -private fun RecyclerView.adapter() = adapter as? Adapter - ?: error("Adapter not set or wrong adapter set to recyclerview.") - -@Composable -fun ReorderButton( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - CircleButtonFilled( - modifier = modifier - .testTag("reorder_button"), - icon = R.drawable.ic_reorder, - onClick = onClick - ) -} - - -@Suppress("UNCHECKED_CAST") -private fun calculateOrderNum( - itemsInNewOrder: List<*>, - to: Int -): Double { - val itemBefore = itemsInNewOrder.getOrNull(to - 1) as? T - val itemAfter = itemsInNewOrder.getOrNull(to + 1) as? T - - return when { - itemBefore != null && itemAfter != null -> { - numberBetween( - itemBefore.getItemOrderNum(), - itemAfter.getItemOrderNum() - ) - } - itemBefore != null && itemAfter == null -> { - //It's last in it's priority - itemBefore.getItemOrderNum() + 1 - } - itemBefore == null && itemAfter != null -> { - //It's first in it's priority - itemAfter.getItemOrderNum() - 1 - } - else -> 0.0 - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/WrapContentRow.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/WrapContentRow.kt deleted file mode 100644 index d3b00a9f6c..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/WrapContentRow.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.ivy.wallet.ui.theme.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.ivy.data.CategoryOld -import com.ivy.design.l0_system.UI -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Ivy - - -@Composable -fun WrapContentRow( - modifier: Modifier = Modifier, - items: List, - verticalMarginBetweenRows: Dp = 8.dp, - horizontalMarginBetweenItems: Dp = 8.dp, - ItemContent: @Composable (item: T) -> Unit -) { - if (items.isEmpty()) return - - Layout( - modifier = modifier, - content = { - for (item in items) { - ItemContent(item) - } - } - ) { measurables, constraints -> - val childConstraints = constraints.copy(minWidth = 0, minHeight = 0) - - var x = 0 - - val placeables = measurables.map { - it.measure(childConstraints) - } - val itemHeight = placeables.maxOfOrNull { it.height } ?: 0 - - var height = 0 - - for (placeable in placeables) { - if (x + placeable.width > constraints.maxWidth) { - //item is overflowing -> move it to a new row - x = 0 - height += itemHeight + verticalMarginBetweenRows.roundToPx() - x += placeable.width + horizontalMarginBetweenItems.roundToPx() - continue - } - - x += placeable.width + horizontalMarginBetweenItems.roundToPx() - } - - height += itemHeight - - - layout(constraints.maxWidth, height) { - //Reset x - x = 0 - var y = 0 - - placeables.forEach { placeable -> - if (x + placeable.width > constraints.maxWidth) { - //item is overflowing -> move it to a new row - x = 0 - y += itemHeight + verticalMarginBetweenRows.roundToPx() - } - - placeable.place(x, y) - x += placeable.width + horizontalMarginBetweenItems.roundToPx() - } - } - } -} - -@Preview -@Composable -private fun PreviewWrapContentRow() { - IvyPreview { - WrapContentRow( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 24.dp) - .background(UI.colors.red), - items = listOf( - CategoryOld("Todo", color = Ivy.toArgb()), - CategoryOld("Ivy", color = Ivy.toArgb()), - CategoryOld("Qredo", color = Ivy.toArgb()), - CategoryOld("Home", color = Ivy.toArgb()), - CategoryOld("Inspiration", color = Ivy.toArgb()), - CategoryOld("Business and marketing", color = Ivy.toArgb()), - CategoryOld("Testdfsgdfgdf", color = Ivy.toArgb()), - ), - verticalMarginBetweenRows = 8.dp - ) { - Text( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(UI.colors.medium, RoundedCornerShape(8.dp)) - .clickable(onClick = { }) - .padding(horizontal = 20.dp, vertical = 12.dp), - text = it.name, - style = TextStyle( - color = UI.colorsInverted.medium, - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold - ) - ) - - Spacer(modifier = Modifier.width(8.dp)) - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/charts/linechart/Grid.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/charts/linechart/Grid.kt deleted file mode 100644 index 4c06c4e23f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/charts/linechart/Grid.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.ivy.wallet.ui.theme.components.charts.linechart - diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/components/charts/linechart/IvyLineChart.kt b/ui-components-old/src/main/java/com/ivy/old/theme/components/charts/linechart/IvyLineChart.kt deleted file mode 100644 index 818d60e9f7..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/components/charts/linechart/IvyLineChart.kt +++ /dev/null @@ -1,686 +0,0 @@ -package com.ivy.wallet.ui.theme.components.charts.linechart - -import android.text.TextPaint -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.ivy.core.ui.temp.trash.Month -import com.ivy.design.l0_system.UI -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.utils.lerp -import timber.log.Timber -import java.text.DecimalFormat -import kotlin.math.pow -import kotlin.math.sqrt - - -data class Value( - val x: Double, - val y: Double -) - -data class Function( - val values: List, - val color: Color, - val colorDown: Color? = null, -) { - fun determineLineColor(valueStart: Double, valueEnd: Double): Color { - return if (colorDown != null) { - if (valueStart <= valueEnd) color else colorDown - } else { - color - } - } -} - -data class TapEvent( - val functionIndex: Int, - val valueIndex: Int -) - -data class FunctionPoint( - val functionIndex: Int, - val valueIndex: Int, - val point: Offset -) - -private fun yValues( - min: Double, - max: Double -): List { - val center = (min + max) / 2 - val centerTop = (center + max) / 2 - val centerBottom = (center + min) / 2 - - return listOf( - max, - centerTop, - center, - centerBottom, - min - ) -} - - -private fun DrawScope.drawTappedPoint( - functions: List, - tapEvent: TapEvent?, - chartWidth: Float, - chartHeight: Float, - minY: Double, - maxY: Double -) { - tapEvent?.let { - val tappedValue = functions - .getOrNull(it.functionIndex)?.values - ?.get(it.valueIndex) - ?: return@let - val radius = 8.dp.toPx() - - drawCircle( - color = Ivy, - radius = radius, - center = Offset( - x = calculateXCoordinate( - values = functions[it.functionIndex].values, - valueIndex = it.valueIndex, - chartWidth = chartWidth - ), - y = calculateYCoordinate( - max = maxY, - min = minY, - value = tappedValue.y, - chartHeight = chartHeight, - offsetTop = 0f, //TODO: Fix - offsetBottom = 0f //TODO: Fix - ) - 4.dp.toPx() //marginFromX //TODO: FIX - ) - ) - } -} - -private fun DrawScope.drawFunctions( - chartWidth: Float, - lineDistance: Float, - chartHeight: Float, - offsetLeft: Float = 0f, - offsetTop: Float = 0f, - offsetBottom: Float = 0f, - cellSize: Float, - maxY: Double, - minY: Double, - functions: List, -): List { - // Add some kind of a "Padding" for the initial point where the line starts. - val lineWidth = 3.dp.toPx() - val marginFromX = 4.dp.toPx() - - - return functions.flatMapIndexed { index, function -> - drawFunction( - function = function, - functionIndex = index, - minY = minY, - maxY = maxY, - cellSize = cellSize, - lineDistance = lineDistance, - lineWidth = lineWidth, - chartHeight = chartHeight, - offsetLeft = offsetLeft, - offsetTop = offsetTop, - offsetBottom = offsetBottom - ) - } -} - -private fun DrawScope.drawFunction( - function: Function, - functionIndex: Int, - minY: Double, - maxY: Double, - chartHeight: Float, - cellSize: Float, - lineDistance: Float, - lineWidth: Float, - offsetLeft: Float, - offsetTop: Float, - offsetBottom: Float, -): List { - val points = mutableListOf() - - var currentX = offsetLeft - val values = function.values - val totalRecords = values.size - - values.forEachIndexed { index, value -> - if (totalRecords >= index + 2) { - val valueStart = value.y - val valueEnd = values[index + 1].y - - val pointStart = Offset( - x = currentX, - y = calculateYCoordinate( - max = maxY, - min = minY, - value = valueStart, - chartHeight = chartHeight, - offsetTop = offsetTop, - offsetBottom = offsetBottom - ) - ) - val pointEnd = Offset( - x = currentX + lineDistance, - y = calculateYCoordinate( - max = maxY, - min = minY, - value = valueEnd, - chartHeight = chartHeight, - offsetTop = offsetTop, - offsetBottom = offsetBottom - ) - ) - - if (index == 0) { - points.add( - FunctionPoint( - functionIndex = functionIndex, - valueIndex = index, - point = pointStart - ) - ) - } - - points.add( - FunctionPoint( - functionIndex = functionIndex, - valueIndex = index + 1, - point = pointEnd - ) - ) - - drawLine( - start = pointStart, - end = pointEnd, - color = function.determineLineColor( - valueStart = valueStart, - valueEnd = valueEnd - ), - strokeWidth = lineWidth, - pathEffect = PathEffect.cornerPathEffect(8.dp.toPx()), - cap = StrokeCap.Round - ) - } - - currentX += lineDistance - } - - return points -} - - -private fun calculateXCoordinate( - values: List, - valueIndex: Int, - chartWidth: Float, -): Float { - val totalRecords = values.size - val lineDistance = chartWidth / (totalRecords + 1) - - return lineDistance * valueIndex + lineDistance -} - - -private fun calculateYCoordinate( - max: Double, - min: Double, - value: Double, - chartHeight: Float, - offsetTop: Float, - offsetBottom: Float -): Float { - //Lerp: (start + x * (end - start)) = value - //x * (end - start) = value - start - //x = (value - start) / (end - start) - val yPercent = (value - min) / (max - min) - - return lerp( - start = offsetTop.toDouble(), - end = (chartHeight - offsetBottom).toDouble(), - fraction = 1f - yPercent - ).toFloat() -} - -private fun Offset.distance(point2: Offset): Float { - return sqrt((point2.x - x).pow(2) + (point2.y - y).pow(2)) -} - - -@Composable -fun IvyLineChart( - modifier: Modifier = Modifier, - height: Dp = 300.dp, - functions: List, - title: String, - xLabel: (x: Double) -> String, - yLabel: (y: Double) -> String, - onTap: (TapEvent) -> Unit = {} -) { - val allValues = functions.flatMap { it.values } - if (allValues.isEmpty()) return - - val maxY = allValues.maxOf { it.y } - val minY = allValues.minOf { it.y } - - var tapEvent: TapEvent? by remember { - mutableStateOf(null) - } - val onTapInternal = { event: TapEvent -> - tapEvent = event - onTap(event) - } - - IvyChart( - modifier = modifier - .fillMaxWidth() - .height(height) - .clip(UI.shapes.rounded) - .border(2.dp, Gray, UI.shapes.rounded), - title = title, - allValues = allValues, - xLabel = xLabel, - yLabel = yLabel, - maxY = maxY, - minY = minY, - functions = functions, - tapEvent = tapEvent, - onTap = onTapInternal - ) -} - -@Composable -private fun IvyChart( - modifier: Modifier, - title: String, - allValues: List, - xLabel: (x: Double) -> String, - yLabel: (y: Double) -> String, - maxY: Double, - minY: Double, - functions: List, - tapEvent: TapEvent?, - onTap: (TapEvent) -> Unit -) { - var points: List by remember { - mutableStateOf(emptyList()) - } - - val xLabelColor = UI.colorsInverted.pure - - Canvas( - modifier = modifier - .pointerInput(Unit) { - detectTapGestures( - onTap = { clickPoint -> - val targetPoint = points.minByOrNull { - clickPoint.distance(it.point) - } ?: return@detectTapGestures - - Timber.i("points.size = ${points.size}") - - onTap( - TapEvent( - functionIndex = targetPoint.functionIndex, - valueIndex = targetPoint.valueIndex - ) - ) - } - ) - } - ) { - val chartWidth = size.width - val chartHeight = size.height - - val offsetCellsLeft = 2 - val offsetCellsRight = 1 - - val totalRecords = functions.first().values.size - val xDistance = chartWidth / (totalRecords + offsetCellsLeft + offsetCellsRight) - - - val cellSize = xDistance - val offsetTop = cellSize * 3 - val offsetBottom = cellSize - - - - drawTitle( - title = title, - cellSize = cellSize, - chartWidth = chartWidth - ) - - drawXLabels( - cellSize = cellSize, - offsetLeft = offsetCellsLeft * cellSize, - offsetRight = offsetCellsRight * cellSize, - lineDistance = xDistance, - chartHeight = chartHeight, - allValues = allValues, - textColor = xLabelColor, - xLabel = xLabel - ) - - drawYValues( - chartHeight = chartHeight, - offsetBottom = offsetBottom, - offsetTop = offsetTop, - cellSize = cellSize, - maxY = maxY, - minY = minY, - yLabel = yLabel - ) - - grid( - chartWidth = chartWidth, - chartHeight = chartHeight, - cellSize = cellSize - ) - - points = drawFunctions( - chartWidth = chartWidth, - chartHeight = chartHeight, - maxY = maxY, - minY = minY, - cellSize = cellSize, - offsetLeft = offsetCellsLeft * cellSize, - offsetTop = offsetTop, - offsetBottom = offsetBottom, - functions = functions, - lineDistance = xDistance - ) - - drawTappedPoint( - functions = functions, - tapEvent = tapEvent, - chartWidth = chartWidth, - chartHeight = chartHeight, - minY = minY, - maxY = maxY - ) - } -} - -fun DrawScope.drawTitle( - title: String, - cellSize: Float, - chartWidth: Float -) { - drawText( - text = title, - x = chartWidth / 2f, - y = cellSize + 4.dp.toPx(), - textSize = 16.sp - ) -} - -fun DrawScope.drawYValues( - minY: Double, - maxY: Double, - offsetTop: Float, - chartHeight: Float, - offsetBottom: Float, - yLabel: (y: Double) -> String, - cellSize: Float -) { - val yValues = yValues( - min = minY, - max = maxY - ) - - val coordsMinY = chartHeight - offsetBottom - val coordsMaxY = offsetTop - - val centerY = (coordsMinY + coordsMaxY) / 2 - val centerTopY = (centerY + coordsMaxY) / 2 - val centerBottomY = (centerY + coordsMinY) / 2 - - val yCoords = listOf( - coordsMaxY, - centerTopY, - centerY, - centerBottomY, - coordsMinY - ) - - for ((index, value) in yValues.withIndex()) { - drawText( - text = yLabel(value), - x = cellSize, - y = yCoords[index], - textColor = Gray, - textSize = 12.sp - ) - } -} - -fun DrawScope.drawXLabels( - cellSize: Float, - offsetLeft: Float, - offsetRight: Float, - lineDistance: Float, - chartHeight: Float, - allValues: List, - xLabel: (x: Double) -> String, - textColor: Color -) { - allValues.map { it.x }.toSet().forEachIndexed { index, x -> - drawText( - text = xLabel(x), - x = offsetLeft + (index * lineDistance), - y = chartHeight - cellSize / 2f, - textSize = 12.sp, - textColor = textColor - ) - } -} - -fun DrawScope.drawText( - text: String, - x: Float, - y: Float, - textColor: Color = Gray, - textSize: TextUnit, -) { - val textPaint = TextPaint() - textPaint.isAntiAlias = true - textPaint.textSize = textSize.toPx() - textPaint.color = textColor.toArgb() - - val textWidth = textPaint.measureText(text).toInt() - - drawIntoCanvas { - it.nativeCanvas.drawText( - text, - x - textWidth / 2f, - y, - textPaint - ) - } -} - -private fun DrawScope.grid( - chartWidth: Float, - chartHeight: Float, - cellSize: Float //24.dp -) { - verticalLineXS( - chartWidth = chartWidth, - cellSize = cellSize - ).forEach { x -> - drawLine( - color = Gray, - start = Offset( - x = x, - y = 0f - ), - end = Offset( - x = x, - y = chartHeight - ) - ) - } - - horizontalLineYS( - chartHeight = chartHeight, - cellSize = cellSize - ).forEach { y -> - drawLine( - color = Gray, - start = Offset( - x = 0f, - y = y - ), - end = Offset( - x = chartWidth, - y = y - ) - ) - } -} - -private fun verticalLineXS( - chartWidth: Float, - cellSize: Float, - accumulator: List = emptyList(), -): List { - val last = accumulator.lastOrNull() - return if (cellSize >= chartWidth || (last != null && last >= chartWidth)) { - accumulator - } else { - //recurse - val next = (last ?: 0f) + cellSize - - verticalLineXS( - chartWidth = chartWidth, - cellSize = cellSize, - accumulator = accumulator + next - ) - } -} - -private tailrec fun horizontalLineYS( - chartHeight: Float, - cellSize: Float, - accumulator: List = emptyList() -): List { - val last = accumulator.lastOrNull() - return if (cellSize >= chartHeight || (last != null && last <= 0)) { - accumulator - } else { - //recurse - val next = (last ?: chartHeight) - cellSize - - horizontalLineYS( - chartHeight = chartHeight, - cellSize = cellSize, - accumulator = accumulator + next - ) - } -} - - -@Preview -@Composable -private fun Preview_IvyChart() { - ComponentPreview { - val values = listOf( - Value( - x = 0.0, - y = 5235.60 - ), - Value( - x = 1.0, - y = 8000.0 - ), - Value( - x = 2.0, - y = 15032.89 - ), - Value( - x = 3.0, - y = 4123.0 - ), - Value( - x = 4.0, - y = 1000.0 - ), - Value( - x = 5.0, - y = -5000.0 - ), - Value( - x = 6.0, - y = 3000.0 - ), - Value( - x = 7.0, - y = 9000.0 - ), - Value( - x = 8.0, - y = 15600.50 - ), - Value( - x = 9.0, - y = 20000.0 - ), - Value( - x = 10.0, - y = 0.0 - ), - Value( - x = 11.0, - y = 1000.0 - ), - ) - - IvyLineChart( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - title = "EXPENSES LAST 12 MONTHS", - height = 400.dp, - functions = listOf( - Function( - values = values, - color = Green - ) - ), - xLabel = { - Month.monthsList()[it.toInt()].name.first().toString() - }, - yLabel = { - DecimalFormat("#,###").format(it) - } - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/AddKeywordModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/AddKeywordModal.kt deleted file mode 100644 index 2409450de2..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/AddKeywordModal.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.ui.theme.components.IvyTitleTextField -import com.ivy.wallet.utils.selectEndTextFieldValue -import java.util.* - -@Composable -fun BoxWithConstraintsScope.AddKeywordModal( - id: UUID = UUID.randomUUID(), - keyword: String, - visible: Boolean, - dismiss: () -> Unit, - onKeywordChanged: (String) -> Unit -) { - var modalKeyword by remember(id) { mutableStateOf(selectEndTextFieldValue(keyword)) } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalAdd { - onKeywordChanged(modalKeyword.text) - dismiss() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.add_keyword), - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.height(32.dp)) - - val inputFocus = FocusRequester() - - IvyTitleTextField( - modifier = Modifier - .padding(horizontal = 32.dp) - .focusRequester(inputFocus), - dividerModifier = Modifier.padding(horizontal = 24.dp), - value = modalKeyword, - hint = stringResource(R.string.keyword) - ) { - modalKeyword = it - } - - Spacer(Modifier.height(48.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - AddKeywordModal( - visible = true, - keyword = "", - dismiss = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/BudgetModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/BudgetModal.kt deleted file mode 100644 index 3c46410d27..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/BudgetModal.kt +++ /dev/null @@ -1,349 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.BudgetExt -import com.ivy.core.ui.temp.trash.parseAccountIds -import com.ivy.core.ui.temp.trash.parseCategoryIds -import com.ivy.data.AccountOld -import com.ivy.data.Budget -import com.ivy.data.CategoryOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.old.ListItem -import com.ivy.wallet.domain.deprecated.logic.model.CreateBudgetData -import com.ivy.wallet.ui.theme.Green -import com.ivy.wallet.ui.theme.Purple1Dark -import com.ivy.wallet.ui.theme.Red3Light -import com.ivy.wallet.ui.theme.components.IvyNameTextField -import com.ivy.wallet.ui.theme.modal.edit.AmountModal -import com.ivy.wallet.ui.theme.toComposeColor -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank -import com.ivy.wallet.utils.selectEndTextFieldValue -import java.util.* - - -data class BudgetModalData( - val budget: Budget?, - - val baseCurrency: String, - val categories: List, - val accounts: List, - - val id: UUID = UUID.randomUUID(), - val autoFocusKeyboard: Boolean = true, -) - -@Composable -fun BoxWithConstraintsScope.BudgetModal( - modal: BudgetModalData?, - - onCreate: (CreateBudgetData) -> Unit, - onEdit: (Budget) -> Unit, - onDelete: (Budget) -> Unit, - dismiss: () -> Unit -) { - val initialBudget = modal?.budget - var nameTextFieldValue by remember(modal) { - mutableStateOf(selectEndTextFieldValue(initialBudget?.name)) - } - var amount by remember(modal) { - mutableStateOf(initialBudget?.amount ?: 0.0) - } - var categoryIds by remember(modal) { - mutableStateOf(modal?.budget?.parseCategoryIds() ?: emptyList()) - } - var accountIds by remember(modal) { - mutableStateOf(modal?.budget?.parseAccountIds() ?: emptyList()) - } - - - var amountModalVisible by remember(modal) { mutableStateOf(false) } - var deleteModalVisible by remember(modal) { mutableStateOf(false) } - - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - PrimaryAction = { - ModalAddSave( - item = modal?.budget, - enabled = nameTextFieldValue.text.isNotNullOrBlank() && amount > 0.0 - ) { - if (initialBudget != null) { - onEdit( - initialBudget.copy( - name = nameTextFieldValue.text.trim(), - amount = amount, - categoryIdsSerialized = BudgetExt.serialize(categoryIds), - accountIdsSerialized = BudgetExt.serialize(accountIds) - ) - ) - } else { - onCreate( - CreateBudgetData( - name = nameTextFieldValue.text.trim(), - amount = amount, - categoryIdsSerialized = BudgetExt.serialize(categoryIds), - accountIdsSerialized = BudgetExt.serialize(accountIds) - ) - ) - } - - dismiss() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - ModalTitle( - text = if (modal?.budget != null) stringResource(R.string.edit_budget) else stringResource( - R.string.create_budget - ) - ) - - if (initialBudget != null) { - Spacer(Modifier.weight(1f)) - - ModalDelete { - deleteModalVisible = true - } - - Spacer(Modifier.width(24.dp)) - } - } - - - Spacer(Modifier.height(24.dp)) - - ModalNameInput( - hint = stringResource(R.string.budget_name), - autoFocusKeyboard = modal?.autoFocusKeyboard ?: true, - textFieldValue = nameTextFieldValue, - setTextFieldValue = { - nameTextFieldValue = it - } - ) - - Spacer(Modifier.height(24.dp)) - - CategoriesRow( - categories = modal?.categories ?: emptyList(), - budgetCategoryIds = categoryIds, - onSetBudgetCategoryIds = { - categoryIds = it - } - ) - - Spacer(Modifier.height(24.dp)) - - ModalAmountSection( - label = stringResource(R.string.budget_amount_uppercase), - currency = modal?.baseCurrency ?: "", - amount = amount, - amountPaddingTop = 24.dp, - amountPaddingBottom = 0.dp - ) { - amountModalVisible = true - } - } - - val amountModalId = remember(modal, amount) { - UUID.randomUUID() - } - AmountModal( - id = amountModalId, - visible = amountModalVisible, - currency = modal?.baseCurrency ?: "", - initialAmount = amount, - dismiss = { amountModalVisible = false } - ) { - amount = it - } - - DeleteModal( - visible = deleteModalVisible, - title = stringResource(R.string.confirm_deletion), - description = stringResource( - R.string.confirm_budget_deletion_warning, - nameTextFieldValue.text - ), - dismiss = { deleteModalVisible = false } - ) { - if (initialBudget != null) { - onDelete(initialBudget) - } - deleteModalVisible = false - dismiss() - } -} - -@Composable -fun ModalNameInput( - hint: String, - autoFocusKeyboard: Boolean, - - textFieldValue: TextFieldValue, - setTextFieldValue: (TextFieldValue) -> Unit, -) { - val nameFocus = FocusRequester() - - val view = LocalView.current - IvyNameTextField( - modifier = Modifier - .padding(start = 32.dp, end = 36.dp) - .focusRequester(nameFocus), - underlineModifier = Modifier.padding(start = 32.dp, end = 32.dp), - value = textFieldValue, - hint = hint, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Words, - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Text, - autoCorrect = true - ), - keyboardActions = KeyboardActions( - onDone = { - hideKeyboard(view) - } - ), - ) { newValue -> - setTextFieldValue(newValue) - } -} - -@Composable -private fun CategoriesRow( - categories: List, - budgetCategoryIds: List, - - onSetBudgetCategoryIds: (List) -> Unit, -) { - Text( - modifier = Modifier.padding(start = 32.dp), - text = BudgetExt.type(budgetCategoryIds.size), - style = UI.typo.b1.style( - fontWeight = FontWeight.Medium, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.height(16.dp)) - - LazyRow( - modifier = Modifier.testTag("budget_categories_row") - ) { - item { - Spacer(Modifier.width(24.dp)) - } - - items(items = categories) { category -> - ListItem( - icon = category.icon, - defaultIcon = R.drawable.ic_custom_category_s, - text = category.name, - selectedColor = category.color.toComposeColor().takeIf { - budgetCategoryIds.contains(category.id) - } - ) { selected -> - if (selected) { - //remove category - onSetBudgetCategoryIds(budgetCategoryIds.filter { it != category.id }) - } else { - //add category - onSetBudgetCategoryIds(budgetCategoryIds.plus(category.id)) - } - } - } - - item { - Spacer(Modifier.width(24.dp)) - } - } -} - -@Preview -@Composable -private fun Preview_create() { - IvyPreview { - val cat1 = CategoryOld("Science", color = Purple1Dark.toArgb(), icon = "atom") - - BudgetModal( - modal = BudgetModalData( - budget = null, - baseCurrency = "BGN", - categories = listOf( - cat1, - CategoryOld("Pet", color = Red3Light.toArgb(), icon = "pet"), - CategoryOld("Home", color = Green.toArgb(), icon = null), - ), - accounts = emptyList() - ), - onCreate = {}, - onEdit = {}, - onDelete = {} - ) { - - } - } -} - -@Preview -@Composable -private fun Preview_edit() { - IvyPreview { - val cat1 = CategoryOld("Science", color = Purple1Dark.toArgb(), icon = "atom") - - BudgetModal( - modal = BudgetModalData( - budget = Budget( - name = "Shopping", - amount = 1250.0, - accountIdsSerialized = null, - categoryIdsSerialized = null, - orderId = 0.0 - ), - baseCurrency = "BGN", - categories = listOf( - cat1, - CategoryOld("Pet", color = Red3Light.toArgb(), icon = "pet"), - CategoryOld("Home", color = Green.toArgb(), icon = null), - ), - accounts = emptyList() - ), - onCreate = {}, - onEdit = {}, - onDelete = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/BufferModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/BufferModal.kt deleted file mode 100644 index 3c317e4a8f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/BufferModal.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.wallet.ui.theme.components.BufferBattery -import com.ivy.wallet.ui.theme.modal.edit.AmountModal -import java.util.* -import com.ivy.base.R - - -data class BufferModalData( - val balance: Double, - val buffer: Double, - val currency: String, - val id: UUID = UUID.randomUUID() -) - -@Composable -fun BoxWithConstraintsScope.BufferModal( - modal: BufferModalData?, - dismiss: () -> Unit, - onBufferChanged: (Double) -> Unit -) { - var newBufferAmount by remember(modal) { - mutableStateOf(modal?.buffer ?: 0.0) - } - - var amountModalVisible by remember { mutableStateOf(false) } - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - PrimaryAction = { - ModalSave { - onBufferChanged(newBufferAmount) - dismiss() - } - } - ) { - Spacer(Modifier.height(16.dp)) - - BufferBattery( - modifier = Modifier.padding(horizontal = 16.dp), - buffer = newBufferAmount, - balance = modal?.balance ?: 0.0, - currency = modal?.currency ?: "", - backgroundNotFilled = UI.colors.medium, - ) - - Spacer(Modifier.height(24.dp)) - - ModalAmountSection( - label = stringResource(R.string.edit_savings_goal), - currency = modal?.currency ?: "", - amount = newBufferAmount - ) { - amountModalVisible = true - } - } - - val amountModalId = remember(modal, newBufferAmount) { - UUID.randomUUID() - } - AmountModal( - id = amountModalId, - visible = amountModalVisible, - currency = modal?.currency ?: "", - initialAmount = newBufferAmount, - dismiss = { amountModalVisible = false } - ) { - newBufferAmount = it - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChooseIconModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChooseIconModal.kt deleted file mode 100644 index 3ccce3c3ed..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChooseIconModal.kt +++ /dev/null @@ -1,721 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.* -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.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l1_buildingBlocks.B1 -import com.ivy.design.l1_buildingBlocks.DividerW -import com.ivy.design.l1_buildingBlocks.SpacerHor -import com.ivy.design.l1_buildingBlocks.SpacerVer -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.components.ItemIconS -import com.ivy.wallet.ui.theme.dynamicContrast -import com.ivy.wallet.utils.thenIf -import java.util.* - -private const val ICON_PICKER_ICONS_PER_ROW = 5 - -@Composable -fun BoxWithConstraintsScope.ChooseIconModal( - visible: Boolean, - initialIcon: String?, - color: Color, - - id: UUID = UUID.randomUUID(), - - dismiss: () -> Unit, - onIconChosen: (String?) -> Unit -) { - var selectedIcon by remember(id) { - mutableStateOf(initialIcon) - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - scrollState = null, - includeActionsRowPadding = false, - PrimaryAction = { - ModalSave( - modifier = Modifier.testTag("choose_icon_save") - ) { - onIconChosen(selectedIcon) - dismiss() - } - } - ) { - val view = LocalView.current - - LazyColumn( - modifier = Modifier.fillMaxSize() - ) { - item { - Spacer(Modifier.height(32.dp)) - - ModalTitle(text = stringResource(R.string.choose_icon)) - - Spacer(Modifier.height(4.dp)) - } - - icons(selectedIcon = selectedIcon, color = color) { - selectedIcon = it - } - - item { - Spacer(Modifier.height(160.dp)) - } - } - } -} - -private fun LazyListScope.icons( - selectedIcon: String?, - color: Color, - - onIconSelected: (String) -> Unit -) { - val icons = ivyIcons() - - iconsR( - icons = icons, - iconsPerRow = ICON_PICKER_ICONS_PER_ROW, - selectedIcon = selectedIcon, - color = color, - onIconSelected = onIconSelected - ) -} - -private tailrec fun LazyListScope.iconsR( - icons: List, - rowAcc: List = emptyList(), - - iconsPerRow: Int, - selectedIcon: String?, - color: Color, - - onIconSelected: (String) -> Unit -) { - if (icons.isNotEmpty()) { - //recurse - - when (val currentItem = icons.first()) { - is IconPickerSection -> { - addIconsRowIfNotEmpty( - rowAcc = rowAcc, - selectedIcon = selectedIcon, - color = color, - onIconSelected = onIconSelected - ) - - item { - Section(title = currentItem.title) - } - - //RECURSE - iconsR( - icons = icons.drop(1), - rowAcc = emptyList(), - - iconsPerRow = iconsPerRow, - selectedIcon = selectedIcon, - color = color, - onIconSelected = onIconSelected - - ) - } - is String -> { - //icon - - if (rowAcc.size == iconsPerRow) { - //recurse and reset acc - - addIconsRowIfNotEmpty( - rowAcc = rowAcc, - selectedIcon = selectedIcon, - color = color, - onIconSelected = onIconSelected - ) - - //RECURSE - iconsR( - icons = icons.drop(1), - rowAcc = emptyList(), - - iconsPerRow = iconsPerRow, - selectedIcon = selectedIcon, - color = color, - onIconSelected = onIconSelected - - ) - } else { - //recurse by filling acc - - //RECURSE - iconsR( - icons = icons.drop(1), - rowAcc = rowAcc + currentItem, - - iconsPerRow = iconsPerRow, - selectedIcon = selectedIcon, - color = color, - onIconSelected = onIconSelected - - ) - } - } - } - } else { - //end recursion - addIconsRowIfNotEmpty( - rowAcc = rowAcc, - selectedIcon = selectedIcon, - color = color, - onIconSelected = onIconSelected - ) - } -} - -private fun LazyListScope.addIconsRowIfNotEmpty( - rowAcc: List, - - selectedIcon: String?, - color: Color, - - onIconSelected: (String) -> Unit -) { - if (rowAcc.isNotEmpty()) { - item { - IconsRow( - icons = rowAcc, - selectedIcon = selectedIcon, - color = color - ) { - onIconSelected(it) - } - - Spacer(Modifier.height(16.dp)) - } - } -} - -@Composable -private fun IconsRow( - icons: List, - selectedIcon: String?, - color: Color, - - onIconSelected: (String) -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - for ((index, icon) in icons.withIndex()) { - Icon( - icon = icon, - selected = selectedIcon == icon, - color = color - ) { - onIconSelected(icon) - } - - if (index < icons.lastIndex && icons.size >= 5) { - Spacer(Modifier.weight(1f)) - } else { - Spacer(Modifier.width(20.dp)) - } - } - - Spacer(Modifier.width(24.dp)) - } -} - -@Composable -private fun Icon( - icon: String, - selected: Boolean, - color: Color, - - onClick: () -> Unit, -) { - ItemIconS( - modifier = Modifier - .clip(CircleShape) - .border(2.dp, if (selected) color else UI.colors.medium, CircleShape) - .thenIf(selected) { - background(color, CircleShape) - } - .clickable { - onClick() - } - .padding(all = 8.dp) - .testTag(icon), - iconName = icon, - tint = if (selected) color.dynamicContrast() else UI.colorsInverted.medium - ) -} - -@Composable -private fun Section( - title: String -) { - SpacerVer(height = 20.dp) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - DividerW() - - SpacerHor(width = 16.dp) - - B1(text = title) - - SpacerHor(width = 16.dp) - - DividerW() - } - - SpacerVer(height = 20.dp) -} - -@Preview -@Composable -private fun ChooseIconModal() { - IvyPreview { - ChooseIconModal( - visible = true, - initialIcon = "gift", - color = Ivy, - dismiss = {} - ) { - - } - } -} - -data class IconPickerSection(val title: String) - -fun ivyIcons(): List = listOf( - IconPickerSection("Ivy"), - "account", - "category", - "cash", - "bank", - "revolut", - "clothes2", - "clothes", - "family", - "star", - "education", - "fitness", - "loan", - "orderfood", - "orderfood2", - "pet", - "restaurant", - "selfdevelopment", - "work", - "vehicle", - "atom", - "bills", - "birthday", - "calculator", - "camera", - "chemistry", - "coffee", - "connect", - "dna", - "doctor", - "document", - "drink", - "farmacy", - "fingerprint", - "fishfood", - "food2", - "fooddrink", - "furniture", - "gambling", - "game", - "gears", - "gift", - "groceries", - "hairdresser", - "health", - "hike", - "house", - "insurance", - "label", - "leaf", - "location", - "makeup", - "music", - "notice", - "people", - "plant", - "programming", - "relationship", - "rocket", - "safe", - "sail", - "server", - "shopping2", - "shopping", - "sports", - "stats", - "tools", - "transport", - "travel", - "trees", - "zeus", - "calendar", - "crown", - "diamond", - "palette", - IconPickerSection("Brands"), - "ic_vue_brands_triangle", - "ic_vue_brands_trello", - "ic_vue_brands_html5", - "ic_vue_brands_spotify", - "ic_vue_brands_bootsrap", - "ic_vue_brands_dribbble", - "ic_vue_brands_google_play", - "ic_vue_brands_dropbox", - "ic_vue_brands_js", - "ic_vue_brands_drive", - "ic_vue_brands_paypal", - "ic_vue_brands_be", - "ic_vue_brands_figma", - "ic_vue_brands_messenger", - "ic_vue_brands_facebook", - "ic_vue_brands_framer", - "ic_vue_brands_whatsapp", - "ic_vue_brands_html3", - "ic_vue_brands_zoom", - "ic_vue_brands_ok", - "ic_vue_brands_twitch", - "ic_vue_brands_youtube", - "ic_vue_brands_apple", - "ic_vue_brands_android", - "ic_vue_brands_slack", - "ic_vue_brands_vuesax", - "ic_vue_brands_blogger", - "ic_vue_brands_photoshop", - "ic_vue_brands_python", - "ic_vue_brands_google", - "ic_vue_brands_xd", - "ic_vue_brands_illustrator", - "ic_vue_brands_xiaomi", - "ic_vue_brands_windows", - "ic_vue_brands_snapchat", - "ic_vue_brands_ui8", - IconPickerSection("Building"), - "ic_vue_building_building1", - "ic_vue_building_buildings", - "ic_vue_building_hospital", - "ic_vue_building_building", - "ic_vue_building_bank", - "ic_vue_building_house", - "ic_vue_building_courthouse", - IconPickerSection("Chart"), - "ic_vue_chart_diagram", - "ic_vue_chart_graph", - "ic_vue_chart_status_up", - "ic_vue_chart_chart", - "ic_vue_chart_trend_up", - IconPickerSection("Crypto"), - "ic_vue_crypto_dent", - "ic_vue_crypto_icon", - "ic_vue_crypto_decred", - "ic_vue_crypto_ocean_protocol", - "ic_vue_crypto_hedera_hashgraph", - "ic_vue_crypto_binance_usd", - "ic_vue_crypto_maker", - "ic_vue_crypto_xrp", - "ic_vue_crypto_harmony", - "ic_vue_crypto_theta", - "ic_vue_crypto_celsius_", - "ic_vue_crypto_vibe", - "ic_vue_crypto_augur", - "ic_vue_crypto_graph", - "ic_vue_crypto_monero", - "ic_vue_crypto_aave", - "ic_vue_crypto_dai", - "ic_vue_crypto_litecoin", - "ic_vue_crypto_tether", - "ic_vue_crypto_thorchain", - "ic_vue_crypto_nexo", - "ic_vue_crypto_chainlink", - "ic_vue_crypto_ethereum_classic", - "ic_vue_crypto_usd_coin", - "ic_vue_crypto_nem", - "ic_vue_crypto_eos", - "ic_vue_crypto_emercoin", - "ic_vue_crypto_dash", - "ic_vue_crypto_ontology", - "ic_vue_crypto_ftx_token", - "ic_vue_crypto_educare", - "ic_vue_crypto_solana", - "ic_vue_crypto_ethereum", - "ic_vue_crypto_velas", - "ic_vue_crypto_hex", - "ic_vue_crypto_polkadot", - "ic_vue_crypto_huobi_token", - "ic_vue_crypto_polyswarm", - "ic_vue_crypto_ankr", - "ic_vue_crypto_enjin_coin", - "ic_vue_crypto_polygon", - "ic_vue_crypto_wing", - "ic_vue_crypto_nebulas", - "ic_vue_crypto_iost", - "ic_vue_crypto_binance_coin", - "ic_vue_crypto_kyber_network", - "ic_vue_crypto_trontron", - "ic_vue_crypto_stellar", - "ic_vue_crypto_avalanche", - "ic_vue_crypto_wanchain", - "ic_vue_crypto_cardano", - "ic_vue_crypto_okb", - "ic_vue_crypto_stacks", - "ic_vue_crypto_siacoin", - "ic_vue_crypto_autonio", - "ic_vue_crypto_civic", - "ic_vue_crypto_zel", - "ic_vue_crypto_quant", - "ic_vue_crypto_tenx", - "ic_vue_crypto_celo", - "ic_vue_crypto_bitcoin", - IconPickerSection("Delivery"), - "ic_vue_delivery_package", - "ic_vue_delivery_receive", - "ic_vue_delivery_box1", - "ic_vue_delivery_box", - "ic_vue_delivery_truck", - IconPickerSection("Design"), - "ic_vue_design_bezier", - "ic_vue_design_brush", - "ic_vue_design_color_swatch", - "ic_vue_design_scissors", - "ic_vue_design_magicpen", - "ic_vue_design_roller", - "ic_vue_design_tool_pen", - IconPickerSection("Dev"), - "ic_vue_dev_code", - "ic_vue_dev_hierarchy", - "ic_vue_dev_relation", - "ic_vue_dev_arrow", - "ic_vue_dev_data", - "ic_vue_dev_hashtag", - IconPickerSection("Education"), - "ic_vue_edu_planer", - "ic_vue_edu_briefcase", - "ic_vue_edu_award", - "ic_vue_edu_glass", - "ic_vue_edu_graduate_cap", - "ic_vue_edu_calculator", - "ic_vue_edu_note", - "ic_vue_edu_magazine", - "ic_vue_edu_pen", - "ic_vue_edu_telescope", - "ic_vue_edu_book", - "ic_vue_edu_ruler_pen", - "ic_vue_edu_todo", - "ic_vue_edu_omega", - "ic_vue_edu_bookmark", - IconPickerSection("Files"), - "ic_vue_files_folder_favorite", - "ic_vue_files_folder", - "ic_vue_files_folder_cloud", - IconPickerSection("Location"), - "ic_vue_location_map1", - "ic_vue_location_map", - "ic_vue_location_location", - "ic_vue_location_global", - "ic_vue_location_global_search", - "ic_vue_location_routing", - "ic_vue_location_discover", - "ic_vue_location_radar", - "ic_vue_location_global_edit", - IconPickerSection("Main"), - "ic_vue_main_cake", - "ic_vue_main_reserve", - "ic_vue_main_archive", - "ic_vue_main_signpost", - "ic_vue_main_coffee", - "ic_vue_main_sport", - "ic_vue_main_notification", - "ic_vue_main_lamp_charge", - "ic_vue_main_home", - "ic_vue_main_judge", - "ic_vue_main_timer", - "ic_vue_main_lamp", - "ic_vue_main_battery_charging", - "ic_vue_main_calendar", - "ic_vue_main_home_wifi", - "ic_vue_main_tree", - "ic_vue_main_battery_half", - "ic_vue_main_send", - "ic_vue_main_glass", - "ic_vue_main_emoji_normal", - "ic_vue_main_share", - "ic_vue_main_trash", - "ic_vue_main_milk", - "ic_vue_main_lifebuoy", - "ic_vue_main_broom", - "ic_vue_main_gift", - "ic_vue_main_clock", - "ic_vue_main_emoji_happy", - "ic_vue_main_home_safe", - "ic_vue_main_crown", - "ic_vue_main_cup", - "ic_vue_main_emoji_sad", - "ic_vue_main_pet", - "ic_vue_main_flash", - IconPickerSection("Media"), - "ic_vue_media_microphone", - "ic_vue_media_music", - "ic_vue_media_voice", - "ic_vue_media_image", - "ic_vue_media_scissors", - "ic_vue_media_mountains", - "ic_vue_media_film", - "ic_vue_media_photocamera", - "ic_vue_media_film_play", - "ic_vue_media_camera", - "ic_vue_media_screenmirroring", - "ic_vue_media_speaker", - "ic_vue_media_play", - "ic_vue_media_subtitle", - "ic_vue_media_setting", - IconPickerSection("Messages"), - "ic_vue_messages_msg_favorite", - "ic_vue_messages_direct", - "ic_vue_messages_msg_notification", - "ic_vue_messages_device_msg", - "ic_vue_messages_edit", - "ic_vue_messages_msgs", - "ic_vue_messages_msg_text", - "ic_vue_messages_letter", - "ic_vue_messages_msg", - "ic_vue_messages_msg_search", - IconPickerSection("Money"), - "ic_vue_money_bitcoin_refresh", - "ic_vue_money_dollar", - "ic_vue_money_archive", - "ic_vue_money_coins", - "ic_vue_money_discount", - "ic_vue_money_recive", - "ic_vue_money_card_send", - "ic_vue_money_buy_crypto", - "ic_vue_money_card_bitcoin", - "ic_vue_money_buy_bitcoin", - "ic_vue_money_ticket_star", - "ic_vue_money_wallet", - "ic_vue_money_send", - "ic_vue_money_ticket_discount", - "ic_vue_money_wallet_cards", - "ic_vue_money_receipt_empty", - "ic_vue_money_percentage", - "ic_vue_money_math", - "ic_vue_money_security_card", - "ic_vue_money_wallet_money", - "ic_vue_money_ticket", - "ic_vue_money_card_receive", - "ic_vue_money_wallet_empty", - "ic_vue_money_transfer", - "ic_vue_money_card_coin", - "ic_vue_money_receipt_items", - "ic_vue_money_tag", - "ic_vue_money_receipt_discount", - "ic_vue_money_card", - IconPickerSection("PC"), - "ic_vue_pc_charging", - "ic_vue_pc_watch", - "ic_vue_pc_headphone", - "ic_vue_pc_gameboy", - "ic_vue_pc_phone_call", - "ic_vue_pc_setting", - "ic_vue_pc_monitor", - "ic_vue_pc_cpu", - "ic_vue_pc_printer", - "ic_vue_pc_bluetooth", - "ic_vue_pc_wifi", - "ic_vue_pc_game", - "ic_vue_pc_speaker", - "ic_vue_pc_phone", - IconPickerSection("People"), - "ic_vue_people_2persons", - "ic_vue_people_person_tag", - "ic_vue_people_person_search", - "ic_vue_people_people", - "ic_vue_people_person", - IconPickerSection("Security"), - "ic_vue_security_eye", - "ic_vue_security_shield_security", - "ic_vue_security_key", - "ic_vue_security_alarm", - "ic_vue_security_lock", - "ic_vue_security_password", - "ic_vue_security_radar", - "ic_vue_security_shield_person", - "ic_vue_security_shield", - IconPickerSection("Shop"), - "ic_vue_shop_cart", - "ic_vue_shop_bag", - "ic_vue_shop_barcode", - "ic_vue_shop_bag1", - "ic_vue_shop_shop", - IconPickerSection("Support"), - "ic_vue_support_star", - "ic_vue_support_medal", - "ic_vue_support_dislike", - "ic_vue_support_like_dislike", - "ic_vue_support_smileys", - "ic_vue_support_heart", - "ic_vue_support_like", - IconPickerSection("Transport"), - "ic_vue_transport_bus", - "ic_vue_transport_airplane", - "ic_vue_transport_train", - "ic_vue_transport_ship", - "ic_vue_transport_gas", - "ic_vue_transport_car", - "ic_vue_transport_car_wash", - IconPickerSection("Type"), - "ic_vue_type_link2", - "ic_vue_type_text", - "ic_vue_type_paperclip", - "ic_vue_type_textalign_left", - "ic_vue_type_translate", - "ic_vue_type_textalign_right", - "ic_vue_type_link", - "ic_vue_type_textalign_center", - "ic_vue_type_textalign_justifycenter", - IconPickerSection("Weather"), - "ic_vue_weather_wind", - "ic_vue_weather_cloud", - "ic_vue_weather_flash", - "ic_vue_weather_moon", - "ic_vue_weather_drop", - "ic_vue_weather_cold", - "ic_vue_weather_sun", -) diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChoosePeriodModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChoosePeriodModal.kt deleted file mode 100644 index 83f65ad5e8..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChoosePeriodModal.kt +++ /dev/null @@ -1,514 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.FromToTimeRange -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.LastNTimeRange -import com.ivy.core.ui.temp.trash.Month -import com.ivy.core.ui.temp.trash.Month.Companion.fromMonthValue -import com.ivy.core.ui.temp.trash.Month.Companion.monthsList -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.data.planned.IntervalType -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.CircleButtonFilled -import com.ivy.wallet.ui.theme.components.IntervalPickerRow -import com.ivy.wallet.ui.theme.components.IvyDividerLine -import com.ivy.wallet.utils.dateNowUTC -import com.ivy.wallet.utils.formatDateOnlyWithYear -import com.ivy.wallet.utils.timeNowUTC -import java.time.LocalDateTime -import java.util.* - -data class ChoosePeriodModalData( - val id: UUID = UUID.randomUUID(), - val period: TimePeriod -) - -@Composable -fun BoxWithConstraintsScope.ChoosePeriodModal( - modal: ChoosePeriodModalData?, - - dismiss: () -> Unit, - onPeriodSelected: (TimePeriod) -> Unit -) { - var period by remember(modal) { - mutableStateOf(modal?.period) - } - - val modalScrollState = rememberScrollState() - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - scrollState = modalScrollState, - PrimaryAction = { - ModalSet( - enabled = period != null && period!!.isValid() - ) { - if (period != null) { -// ivyContext.updateSelectedPeriodInMemory(period!!) - dismiss() - onPeriodSelected(period!!) - } - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ChooseMonth( - selectedMonthYear = period?.month?.let { - MonthYear(month = it, year = period?.year ?: dateNowUTC().year) - } - ) { - period = TimePeriod( - month = it.month, - year = it.year - ) - } - - Spacer(Modifier.height(32.dp)) - - IvyDividerLine( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - ) - - Spacer(Modifier.height(32.dp)) - - FromToRange( - timeRange = period?.fromToRange - ) { - period = TimePeriod( - fromToRange = it - ) - } - - Spacer(Modifier.height(32.dp)) - - LastNPeriod( - modalScrollState = modalScrollState, - lastNTimeRange = period?.lastNRange, - ) { - period = TimePeriod( - lastNRange = it - ) - } - - Spacer(Modifier.height(32.dp)) - - AllTime( - timeRange = period?.fromToRange - ) { - period = TimePeriod( - fromToRange = it - ) - } - - Spacer(Modifier.height(24.dp)) - } -} - -@Composable -private fun ChooseMonth( - selectedMonthYear: MonthYear?, - onSelected: (MonthYear) -> Unit, -) { - Text( - modifier = Modifier - .padding(start = 32.dp), - text = stringResource(R.string.choose_month), - style = UI.typo.b1.style( - color = if (selectedMonthYear != null) UI.colorsInverted.pure else Gray, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(24.dp)) - - val currentYear = dateNowUTC().year - val months = remember(currentYear) { - monthsList() - .map { - MonthYear(month = it, year = currentYear - 1) - } - .plus( - monthsList().map { MonthYear(month = it, year = currentYear) } - ) - .plus( - monthsList().map { MonthYear(month = it, year = currentYear + 1) } - ) - } - - val state = rememberLazyListState() - - val coroutineScope = rememberCoroutineScope() - - LazyRow( - state = state, - verticalAlignment = Alignment.CenterVertically - ) { - item { - Spacer(Modifier.width(12.dp)) - } - - items(items = months) { monthYear -> - MonthButton( - selected = monthYear == selectedMonthYear, - text = monthYear.forDisplay(currentYear = currentYear) - ) { - onSelected(monthYear) - } - - Spacer(Modifier.width(12.dp)) - } - } -} - -data class MonthYear( - val month: Month, - val year: Int -) { - fun forDisplay( - currentYear: Int - ): String { - return if (year != currentYear) { - //not current year - "${month.name}, $year" - } else { - //current year - month.name - } - } -} - -@Composable -private fun MonthButton( - modifier: Modifier = Modifier, - selected: Boolean, - text: String, - onClick: () -> Unit -) { - val background = if (selected) GradientIvy else Gradient.solid(UI.colors.medium) - Text( - modifier = modifier - .clip(UI.shapes.fullyRounded) - .background( - brush = background.asHorizontalBrush(), - shape = UI.shapes.fullyRounded - ) - .clickable { - onClick() - } - .padding(horizontal = 24.dp) - .padding( - vertical = 12.dp, - ), - text = text, - style = UI.typo.b2.style( - fontWeight = FontWeight.Bold, - color = if (selected) White else Gray - ) - ) -} - -@Composable -private fun FromToRange( - timeRange: FromToTimeRange?, - onSelected: (FromToTimeRange?) -> Unit, -) { - Text( - modifier = Modifier - .padding(start = 32.dp), - text = stringResource(R.string.or_custom_range), - style = UI.typo.b1.style( - color = if (timeRange != null) UI.colorsInverted.pure else Gray, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - IntervalFromToDate( - border = IntervalBorder.FROM, - dateTime = timeRange?.from, - otherEndDateTime = timeRange?.to - ) { from -> - onSelected( - if (from == null && timeRange?.to == null) { - null - } else { - timeRange?.copy( - from = from - ) ?: FromToTimeRange( - from = from, - to = null - ) - } - ) - } - - Spacer(Modifier.height(12.dp)) - - IntervalFromToDate( - border = IntervalBorder.TO, - dateTime = timeRange?.to, - otherEndDateTime = timeRange?.from - ) { to -> - onSelected( - if (timeRange?.from == null && to == null) { - null - } else { - timeRange?.copy( - to = to - ) ?: FromToTimeRange( - from = null, - to = to - ) - } - ) - } -} - -@Composable -private fun IntervalFromToDate( - border: IntervalBorder, - dateTime: LocalDateTime?, - otherEndDateTime: LocalDateTime?, - onSelected: (LocalDateTime?) -> Unit -) { - - Row( - modifier = Modifier - .padding(horizontal = 24.dp) - .fillMaxWidth() - .clip(UI.shapes.fullyRounded) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - .clickable { -// ivyContext.datePicker( -// minDate = if (border == IntervalBorder.TO) -// otherEndDateTime -// ?.toLocalDate() -// ?.plusDays(1) else null, -// maxDate = if (border == IntervalBorder.FROM) -// otherEndDateTime -// ?.toLocalDate() -// ?.minusDays(1) else null, -// initialDate = dateTime?.toLocalDate() -// ) { -// onSelected(it.atStartOfDay()) -// } - }, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(32.dp)) - - Text( - modifier = Modifier - .padding( - vertical = 16.dp, - ), - text = if (border == IntervalBorder.FROM) stringResource(R.string.from) else stringResource( - R.string.to - ), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = if (dateTime != null) Green else UI.colorsInverted.pure - ) - ) - - if (dateTime != null) { - Spacer(Modifier.width(16.dp)) - } else { - Spacer(Modifier.weight(1f)) - } - - Text( - text = dateTime?.toLocalDate()?.formatDateOnlyWithYear() - ?: stringResource(R.string.add_date), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.Bold, - color = if (dateTime != null) UI.colorsInverted.pure else Gray - ) - ) - - if (dateTime != null) { - Spacer(Modifier.weight(1f)) - - CircleButtonFilled( - icon = R.drawable.ic_dismiss - ) { - onSelected(null) - } - - Spacer(Modifier.width(4.dp)) - } else { - Spacer(Modifier.width(32.dp)) - } - } -} - -private enum class IntervalBorder { - FROM, TO -} - -@Composable -private fun LastNPeriod( - modalScrollState: ScrollState, - lastNTimeRange: LastNTimeRange?, - - onSelected: (LastNTimeRange) -> Unit -) { - val rootView = LocalView.current - val coroutineScope = rememberCoroutineScope() - - Text( - modifier = Modifier - .padding(start = 32.dp), - text = stringResource(R.string.or_in_the_last), - style = UI.typo.b1.style( - color = if (lastNTimeRange != null) UI.colorsInverted.pure else Gray, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - IntervalPickerRow( - intervalN = lastNTimeRange?.periodN ?: 0, - intervalType = lastNTimeRange?.periodType ?: IntervalType.WEEK, - onSetIntervalN = { - onSelected( - lastNTimeRange?.copy( - periodN = it - ) ?: LastNTimeRange( - periodN = it, - periodType = IntervalType.WEEK - ) - ) - }, - onSetIntervalType = { - onSelected( - lastNTimeRange?.copy( - periodType = it - ) ?: LastNTimeRange( - periodN = 1, - periodType = it - ) - ) - } - ) -} - -@Composable -private fun AllTime( - timeRange: FromToTimeRange?, - onSelected: (FromToTimeRange?) -> Unit, -) { - val active = timeRange != null && timeRange.from == null && - timeRange.to != null && timeRange.to!!.isAfter(timeNowUTC()) - - Text( - modifier = Modifier - .padding(start = 32.dp), - text = stringResource(R.string.or_all_time), - style = UI.typo.b1.style( - color = if (active) UI.colorsInverted.pure else Gray, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - MonthButton( - modifier = Modifier.padding(start = 32.dp), - selected = active, - text = if (active) stringResource(R.string.unselect_all_time) else stringResource(R.string.select_all_time) - ) { - onSelected( - if (active) { - null - } else { - FromToTimeRange( - from = null, - to = timeNowUTC().plusDays(1) - ) - } - ) - } -} - -@Preview -@Composable -private fun Preview_MonthSelected() { - IvyPreview { - ChoosePeriodModal( - modal = ChoosePeriodModalData( - period = TimePeriod( - month = fromMonthValue(3) - ) - ), - dismiss = {} - ) { - - } - } -} - -@Preview -@Composable -private fun Preview_FromTo() { - IvyPreview { - ChoosePeriodModal( - modal = ChoosePeriodModalData( - period = TimePeriod( - fromToRange = FromToTimeRange( - from = timeNowUTC(), - to = timeNowUTC().plusDays(35) - ) - ) - ), - dismiss = {} - ) { - - } - } -} - -@Preview -@Composable -private fun Preview_LastN() { - IvyPreview { - ChoosePeriodModal( - modal = ChoosePeriodModalData( - period = TimePeriod( - lastNRange = LastNTimeRange( - periodN = 1, - periodType = IntervalType.WEEK - ) - ) - ), - dismiss = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChooseStartDateOfMonthModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChooseStartDateOfMonthModal.kt deleted file mode 100644 index 6ec2bccd20..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ChooseStartDateOfMonthModal.kt +++ /dev/null @@ -1,228 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.utils.thenIf -import java.util.* - -@Composable -fun BoxWithConstraintsScope.ChooseStartDateOfMonthModal( - id: UUID = UUID.randomUUID(), - visible: Boolean, - selectedStartDateOfMonth: Int, - - dismiss: () -> Unit, - onStartDateOfMonthSelected: (Int) -> Unit, -) { - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle(text = stringResource(R.string.choose_start_date_of_month)) - - Spacer(Modifier.height(32.dp)) - - NumberRow( - selectedNumber = selectedStartDateOfMonth, - fromInclusive = 1, - toInclusive = 5 - ) { - save( - number = it, - onStartDateOfMonthSelected = onStartDateOfMonthSelected, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(16.dp)) - - NumberRow( - selectedNumber = selectedStartDateOfMonth, - fromInclusive = 6, - toInclusive = 10 - ) { - save( - number = it, - onStartDateOfMonthSelected = onStartDateOfMonthSelected, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(16.dp)) - - NumberRow( - selectedNumber = selectedStartDateOfMonth, - fromInclusive = 11, - toInclusive = 15 - ) { - save( - number = it, - onStartDateOfMonthSelected = onStartDateOfMonthSelected, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(16.dp)) - - NumberRow( - selectedNumber = selectedStartDateOfMonth, - fromInclusive = 16, - toInclusive = 20 - ) { - save( - number = it, - onStartDateOfMonthSelected = onStartDateOfMonthSelected, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(16.dp)) - - NumberRow( - selectedNumber = selectedStartDateOfMonth, - fromInclusive = 21, - toInclusive = 25 - ) { - save( - number = it, - onStartDateOfMonthSelected = onStartDateOfMonthSelected, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(16.dp)) - - NumberRow( - selectedNumber = selectedStartDateOfMonth, - fromInclusive = 26, - toInclusive = 30 - ) { - save( - number = it, - onStartDateOfMonthSelected = onStartDateOfMonthSelected, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(16.dp)) - - NumberRow( - selectedNumber = selectedStartDateOfMonth, - fromInclusive = 31, - toInclusive = 31, - ) { - save( - number = it, - onStartDateOfMonthSelected = onStartDateOfMonthSelected, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(8.dp)) - } -} - -private fun save( - number: Int, - - onStartDateOfMonthSelected: (Int) -> Unit, - dismiss: () -> Unit -) { - onStartDateOfMonthSelected(number) - dismiss() -} - -@Composable -private fun ColumnScope.NumberRow( - selectedNumber: Int, - fromInclusive: Int, - toInclusive: Int, - onClick: (Int) -> Unit -) { - Row( - modifier = Modifier - .align(Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - for (number in fromInclusive..toInclusive) { - NumberView( - number = number, - selected = number == selectedNumber - ) { - onClick(it) - } - - Spacer(Modifier.width(20.dp)) - } - - Spacer(Modifier.width(24.dp)) - } -} - -@Composable -private fun NumberView( - number: Int, - selected: Boolean, - onClick: (Int) -> Unit -) { - Box(modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .border(2.dp, if (selected) Ivy else UI.colors.medium, CircleShape) - .thenIf(selected) { - background(Ivy, CircleShape) - } - .clickable { - onClick(number) - }, - contentAlignment = Alignment.Center - ) { - Text( - text = number.toString(), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.ExtraBold, - color = if (selected) White else UI.colorsInverted.pure, - textAlign = TextAlign.Center - ) - ) - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - ChooseStartDateOfMonthModal( - visible = true, - selectedStartDateOfMonth = 1, - dismiss = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/CurrencyModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/CurrencyModal.kt deleted file mode 100644 index 4fd58c13f0..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/CurrencyModal.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.IvyCurrency -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.CurrencyPicker -import java.util.* - -@Composable -fun BoxWithConstraintsScope.CurrencyModal( - title: String, - initialCurrency: IvyCurrency?, - visible: Boolean, - dismiss: () -> Unit, - id: UUID = UUID.randomUUID(), - - onSetCurrency: (String) -> Unit -) { - var currency by remember(id) { - mutableStateOf(initialCurrency ?: IvyCurrency.getDefault()) - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSave( - modifier = Modifier.testTag("set_currency_save") - ) { - onSetCurrency(currency.code) - dismiss() - } - }, - includeActionsRowPadding = false, - scrollState = null - ) { - var keyboardVisible by remember { - mutableStateOf(false) - } - - if (!keyboardVisible) { - Spacer(Modifier.height(32.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - ModalTitle(text = title) - - Spacer(Modifier.weight(1f)) - - Text( - text = stringResource(R.string.supports_crypto), - style = UI.typo.c.style( - fontWeight = FontWeight.ExtraBold, - color = Gray - ) - ) - - Spacer(Modifier.width(32.dp)) - } - } - - Spacer(Modifier.height(24.dp)) - - CurrencyPicker( - modifier = Modifier - .weight(1f), - initialSelectedCurrency = currency, - - includeKeyboardShownInsetSpacer = false, - lastItemSpacer = 120.dp, - onKeyboardShown = { visible -> - keyboardVisible = visible - } - ) { - currency = it - } - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - CurrencyModal( - title = "Set currency", - initialCurrency = null, - visible = true, - dismiss = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/DeleteModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/DeleteModal.kt deleted file mode 100644 index 3b2c00b156..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/DeleteModal.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.Red -import java.util.* - -@Composable -fun BoxWithConstraintsScope.DeleteModal( - id: UUID = UUID.randomUUID(), - title: String, - description: String, - visible: Boolean, - buttonText: String = stringResource(R.string.delete), - iconStart: Int = R.drawable.ic_delete, - dismiss: () -> Unit, - onDelete: () -> Unit, -) { - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalNegativeButton( - text = buttonText, - iconStart = iconStart - ) { - onDelete() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = title, - style = UI.typo.b1.style( - color = Red, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = description, - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Medium - ) - ) - - Spacer(Modifier.height(48.dp)) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModal.kt deleted file mode 100644 index 195793adb0..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModal.kt +++ /dev/null @@ -1,276 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import com.ivy.design.l0_system.UI -import com.ivy.design.util.IvyPreview - - -import com.ivy.wallet.ui.theme.components.ActionsRow -import com.ivy.wallet.ui.theme.components.CloseButton -import com.ivy.wallet.ui.theme.gradientCutBackgroundTop -import com.ivy.wallet.ui.theme.mediumBlur -import com.ivy.wallet.utils.* -import java.util.* -import kotlin.math.roundToInt - - -private const val DURATION_BACKGROUND_BLUR_ANIM = 400 -const val DURATION_MODAL_ANIM = 200 - -@Composable -fun BoxScope.IvyModal( - id: UUID?, - visible: Boolean, - dismiss: () -> Unit, - SecondaryActions: (@Composable () -> Unit)? = null, - PrimaryAction: @Composable () -> Unit, - scrollState: ScrollState? = rememberScrollState(), - shiftIfKeyboardShown: Boolean = true, - includeActionsRowPadding: Boolean = true, - Content: @Composable ColumnScope.() -> Unit -) { - val rootView = LocalView.current - var keyboardShown by remember { mutableStateOf(false) } - - val keyboardShownInsetDp by animateDpAsState( - targetValue = densityScope { - if (keyboardShown) keyboardOnlyWindowInsets().bottom.toDp() else 0.dp - }, - animationSpec = tween(DURATION_MODAL_ANIM) - ) - val navBarPadding by animateDpAsState( - targetValue = densityScope { - if (keyboardShown) 0.dp else navigationBarInsets().bottom.toDp() - }, - animationSpec = tween(DURATION_MODAL_ANIM) - ) - val blurAlpha by animateFloatAsState( - targetValue = if (visible) 1f else 0f, - animationSpec = tween(DURATION_BACKGROUND_BLUR_ANIM), - visibilityThreshold = 0.01f - ) - val modalPercentVisible by animateFloatAsState( - targetValue = if (visible) 1f else 0f, - animationSpec = tween(DURATION_MODAL_ANIM), - visibilityThreshold = 0.01f - ) - - if (visible || blurAlpha > 0.01f) { - Box( - modifier = Modifier - .fillMaxSize() - .alpha(blurAlpha) - .background(mediumBlur()) - .testTag("modal_outside_blur") - .clickable( - onClick = { - hideKeyboard(rootView) - dismiss() - }, - enabled = visible - ) - .zIndex(1000f) - ) - } - - if (visible || modalPercentVisible > 0.01f) { - var actionsRowHeight by remember { mutableStateOf(0) } - - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - val height = placeable.height - val y = height * (1 - modalPercentVisible) - - layout(placeable.width, height) { - placeable.placeRelative( - 0, - y.roundToInt() - ) - } - } - .fillMaxWidth() - .statusBarsPadding() - .padding(top = 24.dp) - .background(UI.colors.pure, UI.shapes.roundedTop) - .consumeClicks() - .thenIf(scrollState != null) { - verticalScroll(scrollState!!) - } - .zIndex(1000f) - ) { - ModalBackHandling( - modalId = id, - visible = visible, - dismiss = dismiss - ) - - Content() - - //Bottom padding - if (includeActionsRowPadding) { - Spacer(Modifier.height(densityScope { actionsRowHeight.toDp() })) - } - - if (shiftIfKeyboardShown) { - Spacer(Modifier.height(keyboardShownInsetDp)) - } - } - - ModalActionsRow( - visible = visible, - modalPercentVisible = modalPercentVisible, - keyboardShownInsetDp = keyboardShownInsetDp, - navBarPadding = navBarPadding, - onHeightChanged = { - actionsRowHeight = it - }, - onClose = { - hideKeyboard(rootView) - dismiss() - }, - SecondaryActions = SecondaryActions, - PrimaryAction = PrimaryAction - ) - } -} - -@Composable -private fun ModalBackHandling( - modalId: UUID?, - visible: Boolean, - dismiss: () -> Unit -) { - AddModalBackHandling( - modalId = modalId, - visible = visible, - action = { - dismiss() - } - ) -} - -@Composable -fun AddModalBackHandling( - modalId: UUID?, - visible: Boolean, - action: () -> Unit -) { - -} - -@Composable -fun ModalActionsRow( - visible: Boolean, - modalPercentVisible: Float, - keyboardShownInsetDp: Dp, - navBarPadding: Dp, - - onHeightChanged: (Int) -> Unit, - - onClose: () -> Unit, - SecondaryActions: (@Composable () -> Unit)? = null, - PrimaryAction: @Composable () -> Unit -) { - if (visible || modalPercentVisible > 0.01f) { -// val ivyContext = com.ivy.core.ui.temp.ivyWalletCtx() - val screenHeight = LocalConfiguration.current.screenHeightDp.dp - - ActionsRow( - modifier = Modifier - .onSizeChanged { - onHeightChanged(it.height) - } - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - val systemOffsetBottom = keyboardShownInsetDp.toPx() - val visibleHeight = placeable.height * modalPercentVisible - val y = screenHeight.toPx() - visibleHeight - systemOffsetBottom - - layout(placeable.width, placeable.height) { - placeable.place( - 0, - y.roundToInt() - ) - } - } - .gradientCutBackgroundTop( - endY = 16.dp - ) - .padding(top = 8.dp, bottom = 12.dp) - .padding(bottom = navBarPadding) - .zIndex(1100f) - ) { - Spacer(Modifier.width(24.dp)) - - CloseButton( - modifier = Modifier.testTag("modal_close_button"), - onClick = onClose - ) - - SecondaryActions?.invoke() - - Spacer(Modifier.weight(1f)) - - PrimaryAction() - - Spacer(Modifier.width(24.dp)) - } - } -} - -@Preview -@Composable -private fun PreviewIvyModal_minimal() { - IvyPreview { - IvyModal( - id = UUID.randomUUID(), - visible = true, - PrimaryAction = { - ModalSave { - - } - }, - dismiss = {} - ) { - Spacer(modifier = Modifier.height(20.dp)) - - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = "My first Ivy Modal" - ) - - ModalPreviewActionRowSpacer() - } - } -} - -@Composable -fun ModalPreviewActionRowSpacer() { - Spacer(Modifier.height(modalPreviewActionRowHeight())) -} - -@Composable -fun modalPreviewActionRowHeight() = 80.dp \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModalComponents.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModalComponents.kt deleted file mode 100644 index 3de2856d60..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModalComponents.kt +++ /dev/null @@ -1,252 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.GradientGreen -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.GradientRed -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.IvyCircleButton -import com.ivy.wallet.ui.theme.components.IvyOutlinedButton - -@Composable -fun ModalDynamicPrimaryAction( - initialEmpty: Boolean, - initialChanged: Boolean, - - testTagSave: String = "tag_save", - testTagDelete: String = "tag_delete", - - onDelete: () -> Unit, - dismiss: () -> Unit, - onSave: () -> Unit -) { - when { - initialEmpty -> { - ModalAdd( - testTag = testTagSave - ) { - onSave() - dismiss() - } - } - else -> { - if (!initialChanged) { - ModalDelete( - testTag = testTagDelete - ) { - onDelete() - dismiss() - } - } else { - ModalSave( - modifier = Modifier.testTag(testTagSave) - ) { - onSave() - dismiss() - } - } - } - } -} - -@Composable -fun ModalSet( - modifier: Modifier = Modifier, - label: String = stringResource(R.string.set), - enabled: Boolean = true, - onClick: () -> Unit -) { - ModalCheck( - modifier = modifier, - label = label, - enabled = enabled, - onClick = onClick - ) -} - -@Composable -fun ModalCheck( - modifier: Modifier = Modifier, - label: String, - enabled: Boolean = true, - onClick: () -> Unit -) { - ModalPositiveButton( - modifier = modifier, - text = label, - iconStart = R.drawable.ic_check, - enabled = enabled, - onClick = onClick - ) -} - -@Composable -fun ModalAddSave( - item: T, - enabled: Boolean = true, - onClick: () -> Unit -) { - if (item != null) { - ModalSave( - enabled = enabled, - onClick = onClick - ) - } else { - ModalAdd( - enabled = enabled, - onClick = onClick - ) - } -} - -@Composable -fun ModalSave( - modifier: Modifier = Modifier, - enabled: Boolean = true, - onClick: () -> Unit -) { - ModalPositiveButton( - modifier = modifier, - text = stringResource(R.string.save), - iconStart = R.drawable.ic_save, - enabled = enabled, - onClick = onClick - ) -} - -@Composable -fun ModalAdd( - enabled: Boolean = true, - testTag: String = "modal_add", - onClick: () -> Unit -) { - ModalPositiveButton( - modifier = Modifier.testTag(testTag), - text = stringResource(R.string.add), - iconStart = R.drawable.ic_plus, - enabled = enabled, - onClick = onClick - ) -} - -@Composable -fun ModalCreate( - enabled: Boolean = true, - onClick: () -> Unit -) { - ModalPositiveButton( - text = stringResource(R.string.create), - iconStart = R.drawable.ic_plus, - enabled = enabled, - onClick = onClick - ) -} - -@Composable -fun ModalNegativeButton( - text: String, - @DrawableRes iconStart: Int, - enabled: Boolean = true, - onClick: () -> Unit, -) { - IvyButton( - text = text, - backgroundGradient = GradientRed, - iconStart = iconStart, - onClick = onClick, - enabled = enabled - ) -} - -@Composable -fun ModalPositiveButton( - modifier: Modifier = Modifier, - text: String, - @DrawableRes iconStart: Int, - enabled: Boolean = true, - onClick: () -> Unit, -) { - IvyButton( - modifier = modifier, - text = text, - backgroundGradient = GradientGreen, - iconStart = iconStart, - onClick = onClick, - enabled = enabled - ) -} - -@Composable -fun ModalPrimaryButton( - text: String, - @DrawableRes iconStart: Int, - enabled: Boolean = true, - onClick: () -> Unit, -) { - IvyButton( - text = text, - backgroundGradient = GradientIvy, - iconStart = iconStart, - onClick = onClick, - enabled = enabled - ) -} - -@Composable -fun ModalDelete( - enabled: Boolean = true, - testTag: String = "modal_delete", - onClick: () -> Unit -) { - IvyCircleButton( - modifier = Modifier - .size(40.dp) - .testTag(testTag), - icon = R.drawable.ic_delete, - backgroundGradient = GradientRed, - enabled = enabled, - tint = White, - onClick = onClick - ) -} - -@Composable -fun ModalTitle( - text: String -) { - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = text, - style = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) -} - -@Composable -fun ModalSkip( - text: String = stringResource(R.string.skip), - onClick: () -> Unit -) { - IvyOutlinedButton( - text = text, - iconStart = null, - solidBackground = true - ) { - onClick() - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModalDomainComponents.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModalDomainComponents.kt deleted file mode 100644 index 62b6df5664..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/IvyModalDomainComponents.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.components.BalanceRow -import com.ivy.wallet.ui.theme.components.IvyDividerLine -import com.ivy.wallet.utils.clickableNoIndication - -@Composable -fun ModalAmountSection( - label: String, - currency: String, - amount: Double, - Header: (@Composable () -> Unit)? = null, - amountPaddingTop: Dp = 48.dp, - amountPaddingBottom: Dp = 48.dp, - showAmountModal: () -> Unit, -) { - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - IvyDividerLine() - - Header?.invoke() - - Spacer(Modifier.height(amountPaddingTop)) - - Text( - text = label, - style = UI.typo.c.style( - color = UI.colors.neutral, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(4.dp)) - - BalanceRow( - modifier = Modifier - .clickableNoIndication { - showAmountModal() - } - .testTag("amount_balance"), - currency = currency, - balance = amount, - - decimalPaddingTop = 8.dp, - spacerDecimal = 4.dp, - spacerCurrency = 8.dp, - - - integerFontSize = 40.sp, - decimalFontSize = 18.sp, - currencyFontSize = 30.sp, - - currencyUpfront = false - ) - - Spacer(Modifier.height(amountPaddingBottom)) - } -} diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/LoanModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/LoanModal.kt deleted file mode 100644 index 9ed11b6744..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/LoanModal.kt +++ /dev/null @@ -1,577 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.* -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.toArgb -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.IvyCurrency -import com.ivy.data.getDefaultFIATCurrency -import com.ivy.data.loan.Loan -import com.ivy.data.loan.LoanType -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.domain.deprecated.logic.model.CreateLoanData -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.ItemIconSDefaultIcon -import com.ivy.wallet.ui.theme.components.IvyCheckboxWithText -import com.ivy.wallet.ui.theme.components.IvyColorPicker -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.modal.edit.AccountModal -import com.ivy.wallet.ui.theme.modal.edit.AccountModalData -import com.ivy.wallet.ui.theme.modal.edit.AmountModal -import com.ivy.wallet.ui.theme.modal.edit.IconNameRow -import com.ivy.wallet.utils.isNotNullOrBlank -import com.ivy.wallet.utils.selectEndTextFieldValue -import com.ivy.wallet.utils.thenIf -import kotlinx.coroutines.launch -import java.util.* - -data class LoanModalData( - val loan: Loan?, - val baseCurrency: String, - val selectedAccount: AccountOld? = null, - val autoFocusKeyboard: Boolean = true, - val autoOpenAmountModal: Boolean = false, - val createLoanTransaction: Boolean = false, - val id: UUID = UUID.randomUUID() -) - -@Composable -fun BoxWithConstraintsScope.LoanModal( - accounts: List = emptyList(), - onCreateAccount: (CreateAccountData) -> Unit = {}, - - modal: LoanModalData?, - onCreateLoan: (CreateLoanData) -> Unit, - onEditLoan: (Loan, Boolean) -> Unit, - onPerformCalculations: () -> Unit = {}, - dismiss: () -> Unit, -) { - val loan = modal?.loan - var nameTextFieldValue by remember(modal) { - mutableStateOf(selectEndTextFieldValue(loan?.name)) - } - var type by remember(modal) { - mutableStateOf(modal?.loan?.type ?: LoanType.BORROW) - } - var amount by remember(modal) { - mutableStateOf(modal?.loan?.amount ?: 0.0) - } - var color by remember(modal) { - mutableStateOf(loan?.color?.let { Color(it) } ?: Ivy) - } - var icon by remember(modal) { - mutableStateOf(loan?.icon) - } - var currencyCode by remember(modal) { - mutableStateOf(modal?.baseCurrency ?: "") - } - - var selectedAcc by remember(modal) { - mutableStateOf(modal?.selectedAccount) - } - - var createLoanTrans by remember(modal) { - mutableStateOf(modal?.createLoanTransaction ?: false) - } - - var accountChangeModal by remember { mutableStateOf(false) } - var amountModalVisible by remember { mutableStateOf(false) } - var currencyModalVisible by remember { mutableStateOf(false) } - var chooseIconModalVisible by remember(modal) { - mutableStateOf(false) - } - - var accountModalData: AccountModalData? by remember { mutableStateOf(null) } - - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - shiftIfKeyboardShown = false, - PrimaryAction = { - ModalAddSave( - item = modal?.loan, - //enabled = nameTextFieldValue.text.isNotNullOrBlank() && amount > 0 && ((createLoanTrans && selectedAcc != null) || !createLoanTrans) - enabled = nameTextFieldValue.text.isNotNullOrBlank() && amount > 0 && selectedAcc != null - ) { - accountChangeModal = - loan != null && modal.selectedAccount != null && currencyCode != (modal.selectedAccount.currency - ?: modal.baseCurrency) - - if (!accountChangeModal) { - save( - loan = loan, - nameTextFieldValue = nameTextFieldValue, - type = type, - color = color, - icon = icon, - amount = amount, - selectedAccount = selectedAcc, - createLoanTransaction = createLoanTrans, - - onCreateLoan = onCreateLoan, - onEditLoan = onEditLoan, - dismiss = dismiss - ) - } - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle( - text = if (modal?.loan != null) stringResource(R.string.edit_loan) else stringResource(R.string.new_loan), - ) - - Spacer(Modifier.height(24.dp)) - - IconNameRow( - hint = stringResource(R.string.loan_name), - defaultIcon = R.drawable.ic_custom_loan_m, - color = color, - icon = icon, - - autoFocusKeyboard = modal?.autoFocusKeyboard ?: true, - - nameTextFieldValue = nameTextFieldValue, - setNameTextFieldValue = { nameTextFieldValue = it }, - showChooseIconModal = { - chooseIconModalVisible = true - } - ) - - Spacer(Modifier.height(24.dp)) - - LoanTypePicker( - type = type, - onTypeSelected = { type = it } - ) - - Spacer(Modifier.height(24.dp)) - - IvyColorPicker( - selectedColor = color, - onColorSelected = { color = it } - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.associated_account), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - AccountsRow( - accounts = accounts, - selectedAccount = selectedAcc, - onSelectedAccountChanged = { - selectedAcc = it - currencyCode = it.currency ?: getDefaultFIATCurrency().currencyCode - }, - onAddNewAccount = { - accountModalData = AccountModalData( - account = null, - baseCurrency = selectedAcc?.currency ?: "USD", - balance = 0.0 - ) - }, - childrenTestTag = "amount_modal_account" - ) - - Spacer(Modifier.height(16.dp)) - - IvyCheckboxWithText( - modifier = Modifier - .padding(start = 16.dp) - .align(Alignment.Start), - text = stringResource(R.string.create_main_transaction), - checked = createLoanTrans - ) { - createLoanTrans = it - } - - Spacer(modifier = Modifier.height(24.dp)) - - ModalAmountSection( - label = stringResource(R.string.enter_loan_amount_uppercase), - currency = currencyCode, - amount = amount, - amountPaddingTop = 40.dp, - amountPaddingBottom = 40.dp, - ) { - amountModalVisible = true - } - } - - val amountModalId = remember(modal, amount) { - UUID.randomUUID() - } - AmountModal( - id = amountModalId, - visible = amountModalVisible, - currency = currencyCode, - initialAmount = amount, - dismiss = { amountModalVisible = false } - ) { newAmount -> - amount = newAmount - } - - CurrencyModal( - title = stringResource(R.string.choose_currency), - initialCurrency = IvyCurrency.fromCode(currencyCode), - visible = currencyModalVisible, - dismiss = { currencyModalVisible = false } - ) { - currencyCode = it - } - - AccountModal( - modal = accountModalData, - onCreateAccount = onCreateAccount, - onEditAccount = { _, _ -> }, - dismiss = { - accountModalData = null - } - ) - - ChooseIconModal( - visible = chooseIconModalVisible, - initialIcon = icon ?: "loan", - color = color, - dismiss = { chooseIconModalVisible = false } - ) { - icon = it - } - - DeleteModal( - visible = accountChangeModal, - title = stringResource(R.string.confirm_account_change), - description = stringResource(R.string.confirm_account_change_warning), - buttonText = stringResource(R.string.confirm), - iconStart = R.drawable.ic_agreed, - dismiss = { - selectedAcc = modal?.selectedAccount ?: selectedAcc - accountChangeModal = false - } - ) { - onPerformCalculations() - save( - loan = loan, - nameTextFieldValue = nameTextFieldValue, - type = type, - color = color, - icon = icon, - amount = amount, - selectedAccount = selectedAcc, - createLoanTransaction = createLoanTrans, - - onCreateLoan = onCreateLoan, - onEditLoan = onEditLoan, - dismiss = dismiss - ) - accountChangeModal = false - } -} - -@Composable -private fun AccountsRow( - modifier: Modifier = Modifier, - accounts: List, - selectedAccount: AccountOld?, - childrenTestTag: String? = null, - onSelectedAccountChanged: (AccountOld) -> Unit, - onAddNewAccount: () -> Unit -) { - val lazyState = rememberLazyListState() - - LaunchedEffect(accounts, selectedAccount) { - if (selectedAccount != null) { - val selectedIndex = accounts.indexOf(selectedAccount) - if (selectedIndex != -1) { - launch { -// if (TestingContext.inTest) return@launch //breaks UI tests - - lazyState.scrollToItem( - index = selectedIndex, //+1 because Spacer width 24.dp - ) - } - } - } - } - - LazyRow( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - state = lazyState - ) { - item { - Spacer(Modifier.width(24.dp)) - } - - itemsIndexed(accounts) { _, account -> - Account( - account = account, - selected = selectedAccount == account, - testTag = childrenTestTag ?: "account" - ) { - onSelectedAccountChanged(account) - } - } - - item { - AddAccount { - onAddNewAccount() - } - } - - item { - Spacer(Modifier.width(24.dp)) - } - } -} - -@Composable -private fun Account( - account: AccountOld, - selected: Boolean, - testTag: String, - onClick: () -> Unit -) { - val accountColor = account.color.toComposeColor() - val textColor = - if (selected) findContrastTextColor(accountColor) else UI.colorsInverted.pure - - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .thenIf(!selected) { - border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - } - .thenIf(selected) { - background(accountColor, UI.shapes.fullyRounded) - } - .clickable(onClick = onClick) - .testTag(testTag), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - ItemIconSDefaultIcon( - iconName = account.icon, - defaultIcon = R.drawable.ic_custom_account_s, - tint = textColor - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier.padding(vertical = 10.dp), - text = account.name, - style = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.width(8.dp)) -} - -@Composable -private fun AddAccount( - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - .clickable(onClick = onClick), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - IvyIcon( - icon = R.drawable.ic_plus, - tint = UI.colorsInverted.pure - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier.padding(vertical = 10.dp), - text = stringResource(R.string.add_account), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.width(8.dp)) -} - -@Composable -private fun ColumnScope.LoanTypePicker( - type: LoanType, - onTypeSelected: (LoanType) -> Unit -) { - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.loan_type), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier - .padding(horizontal = 24.dp) - .fillMaxWidth() - .background(UI.colors.medium, UI.shapes.rounded), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(8.dp)) - - SelectorButton( - selected = type == LoanType.BORROW, - label = stringResource(R.string.borrow_money) - ) { - onTypeSelected(LoanType.BORROW) - } - - Spacer(Modifier.width(8.dp)) - - SelectorButton( - selected = type == LoanType.LEND, - label = stringResource(R.string.lend_money) - ) { - onTypeSelected(LoanType.LEND) - } - - Spacer(Modifier.width(8.dp)) - } -} - -@Composable -private fun RowScope.SelectorButton( - selected: Boolean, - label: String, - onClick: () -> Unit -) { - Text( - modifier = Modifier - .weight(1f) - .clip(UI.shapes.fullyRounded) - .clickable { - onClick() - } - .padding(vertical = 8.dp) - .thenIf(selected) { - background(GradientIvy.asHorizontalBrush(), UI.shapes.fullyRounded) - } - .padding(vertical = 8.dp), - text = label, - style = UI.typo.b2.style( - color = if (selected) White else Gray, - fontWeight = FontWeight.ExtraBold, - textAlign = TextAlign.Center - ) - ) -} - -private fun save( - loan: Loan?, - nameTextFieldValue: TextFieldValue, - type: LoanType, - color: Color, - icon: String?, - amount: Double, - selectedAccount: AccountOld? = null, - createLoanTransaction: Boolean = false, - - onCreateLoan: (CreateLoanData) -> Unit, - onEditLoan: (Loan, Boolean) -> Unit, - dismiss: () -> Unit -) { - if (loan != null) { - onEditLoan( - loan.copy( - name = nameTextFieldValue.text.trim(), - type = type, - amount = amount, - color = color.toArgb(), - icon = icon, - accountId = selectedAccount?.id - ), - createLoanTransaction - ) - } else { - onCreateLoan( - CreateLoanData( - name = nameTextFieldValue.text.trim(), - type = type, - amount = amount, - color = color, - icon = icon, - account = selectedAccount, - createLoanTransaction = createLoanTransaction - ) - ) - } - - dismiss() -} - - -@Preview -@Composable -private fun Preview() { - IvyPreview { - LoanModal( - modal = LoanModalData( - loan = null, - baseCurrency = "BGN", - ), - onCreateLoan = { }, - onEditLoan = { _, _ -> } - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/LoanRecordModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/LoanRecordModal.kt deleted file mode 100644 index aaffedf7e4..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/LoanRecordModal.kt +++ /dev/null @@ -1,569 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.getDefaultFIATCurrency -import com.ivy.data.loan.LoanRecord -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.domain.deprecated.logic.model.CreateLoanRecordData -import com.ivy.wallet.domain.deprecated.logic.model.EditLoanRecordData -import com.ivy.wallet.ui.theme.components.ItemIconSDefaultIcon -import com.ivy.wallet.ui.theme.components.IvyCheckboxWithText -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.components.IvyOutlinedButton -import com.ivy.wallet.ui.theme.findContrastTextColor -import com.ivy.wallet.ui.theme.modal.edit.AccountModal -import com.ivy.wallet.ui.theme.modal.edit.AccountModalData -import com.ivy.wallet.ui.theme.modal.edit.AmountModal -import com.ivy.wallet.ui.theme.toComposeColor -import com.ivy.wallet.utils.* -import kotlinx.coroutines.launch -import java.time.LocalDateTime -import java.util.* - -data class LoanRecordModalData( - val loanRecord: LoanRecord?, - val baseCurrency: String, - val loanAccountCurrencyCode: String? = null, - val selectedAccount: AccountOld? = null, - val createLoanRecordTransaction: Boolean = false, - val isLoanInterest: Boolean = false, - val id: UUID = UUID.randomUUID() -) - -@Composable -fun BoxWithConstraintsScope.LoanRecordModal( - modal: LoanRecordModalData?, - accounts: List = emptyList(), - onCreateAccount: (CreateAccountData) -> Unit = {}, - - onCreate: (CreateLoanRecordData) -> Unit, - onEdit: (EditLoanRecordData) -> Unit, - onDelete: (LoanRecord) -> Unit, - dismiss: () -> Unit -) { - val initialRecord = modal?.loanRecord - var noteTextFieldValue by remember(modal) { - mutableStateOf(selectEndTextFieldValue(initialRecord?.note)) - } - var currencyCode by remember(modal) { - mutableStateOf(modal?.baseCurrency ?: "") - } - var amount by remember(modal) { - mutableStateOf(modal?.loanRecord?.amount ?: 0.0) - } - var dateTime by remember(modal) { - mutableStateOf(modal?.loanRecord?.dateTime ?: timeNowUTC()) - } - var selectedAcc by remember(modal) { - mutableStateOf(modal?.selectedAccount) - } - var createLoanRecordTrans by remember(modal) { - mutableStateOf(modal?.createLoanRecordTransaction ?: false) - } - var loanInterest by remember(modal) { - mutableStateOf(modal?.isLoanInterest ?: false) - } - var reCalculate by remember(modal) { - mutableStateOf(false) - } - var reCalculateVisible by remember(modal) { - mutableStateOf(modal?.loanAccountCurrencyCode != null && modal.loanAccountCurrencyCode != modal.baseCurrency) - } - - var amountModalVisible by remember { mutableStateOf(false) } - var deleteModalVisible by remember(modal) { mutableStateOf(false) } - var accountModalData: AccountModalData? by remember { mutableStateOf(null) } - var accountChangeConformationModal by remember { mutableStateOf(false) } - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - shiftIfKeyboardShown = true, - PrimaryAction = { - ModalAddSave( - item = initialRecord, - enabled = amount > 0 && selectedAcc != null - //enabled = amount > 0 && ((createLoanRecordTrans && selectedAcc != null) || !createLoanRecordTrans) - ) { - accountChangeConformationModal = - initialRecord != null && modal.selectedAccount != null - && modal.baseCurrency != currencyCode && currencyCode != modal.loanAccountCurrencyCode - - if (!accountChangeConformationModal) - save( - loanRecord = initialRecord, - noteTextFieldValue = noteTextFieldValue, - amount = amount, - dateTime = dateTime, - loanRecordInterest = loanInterest, - selectedAccount = selectedAcc, - createLoanRecordTransaction = createLoanRecordTrans, - reCalculateAmount = reCalculate, - - onCreate = onCreate, - onEdit = onEdit, - dismiss = dismiss, - ) - } - } - ) { - Spacer(Modifier.height(32.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - ModalTitle( - text = if (initialRecord != null) stringResource(R.string.edit_record) else stringResource( - R.string.new_record - ) - ) - - if (initialRecord != null) { - Spacer(Modifier.weight(1f)) - - ModalDelete { - deleteModalVisible = true - } - - Spacer(Modifier.width(24.dp)) - } - } - - Spacer(Modifier.height(24.dp)) - - ModalNameInput( - hint = stringResource(R.string.note), - autoFocusKeyboard = false, - textFieldValue = noteTextFieldValue, - setTextFieldValue = { - noteTextFieldValue = it - } - ) - - Spacer(Modifier.height(24.dp)) - - DateTimeRow( - dateTime = dateTime, - onSetDateTime = { - dateTime = it - } - ) - - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = stringResource(R.string.associated_account), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - AccountsRow( - accounts = accounts, - selectedAccount = selectedAcc, - onSelectedAccountChanged = { - currencyCode = it.currency ?: getDefaultFIATCurrency().currencyCode - - reCalculateVisible = - initialRecord?.convertedAmount != null && selectedAcc != null && currencyCode == modal.baseCurrency - //Unchecks the Recalculate Option if Recalculate Checkbox is not visible - reCalculate = !reCalculateVisible - - selectedAcc = it - - }, - onAddNewAccount = { - accountModalData = AccountModalData( - account = null, - baseCurrency = selectedAcc?.currency ?: "USD", - balance = 0.0 - ) - }, - childrenTestTag = "amount_modal_account" - ) - Spacer(Modifier.height(16.dp)) - - IvyCheckboxWithText( - modifier = Modifier - .padding(start = 16.dp) - .align(Alignment.Start), - text = stringResource(R.string.create_main_transaction), - checked = createLoanRecordTrans - ) { - createLoanRecordTrans = it - } - - IvyCheckboxWithText( - modifier = Modifier - .padding(start = 16.dp) - .align(Alignment.Start), - text = stringResource(R.string.mark_as_interest), - checked = loanInterest - ) { - loanInterest = it - } - - if (reCalculateVisible) { - IvyCheckboxWithText( - modifier = Modifier - .padding(start = 16.dp, end = 8.dp) - .align(Alignment.Start), - text = stringResource(R.string.recalculate_amount_with_today_exchange_rates), - checked = reCalculate - ) { - reCalculate = it - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - ModalAmountSection( - label = stringResource(R.string.enter_record_amount_uppercase), - currency = currencyCode, - amount = amount, - amountPaddingTop = 40.dp, - amountPaddingBottom = 40.dp, - ) { - amountModalVisible = true - } - } - - val amountModalId = remember(modal, amount) { - UUID.randomUUID() - } - AmountModal( - id = amountModalId, - visible = amountModalVisible, - currency = currencyCode, - initialAmount = amount, - dismiss = { amountModalVisible = false } - ) { newAmount -> - amount = newAmount - } - - DeleteModal( - visible = deleteModalVisible, - title = stringResource(R.string.confirm_deletion), - description = stringResource(R.string.record_deletion_warning, noteTextFieldValue.text), - dismiss = { deleteModalVisible = false } - ) { - if (initialRecord != null) { - onDelete(initialRecord) - } - deleteModalVisible = false - reCalculate = false - dismiss() - } - - AccountModal( - modal = accountModalData, - onCreateAccount = onCreateAccount, - onEditAccount = { _, _ -> }, - dismiss = { - accountModalData = null - } - ) - - DeleteModal( - visible = accountChangeConformationModal, - title = stringResource(R.string.confirm_account_change), - description = stringResource(R.string.account_change_warning), - buttonText = stringResource(R.string.confirm), - iconStart = R.drawable.ic_agreed, - dismiss = { - selectedAcc = modal?.selectedAccount ?: selectedAcc - accountChangeConformationModal = false - } - ) { - save( - loanRecord = initialRecord, - noteTextFieldValue = noteTextFieldValue, - amount = amount, - dateTime = dateTime, - loanRecordInterest = loanInterest, - selectedAccount = selectedAcc, - createLoanRecordTransaction = createLoanRecordTrans, - reCalculateAmount = reCalculate, - - onCreate = onCreate, - onEdit = onEdit, - dismiss = dismiss, - ) - - accountChangeConformationModal = false - } -} - -@Composable -private fun DateTimeRow( - dateTime: LocalDateTime, - onSetDateTime: (LocalDateTime) -> Unit -) { - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - IvyOutlinedButton( - text = dateTime.formatNicely(), - iconStart = R.drawable.ic_date - ) { -// ivyContext.datePicker( -// initialDate = dateTime.convertUTCtoLocal().toLocalDate() -// ) { -// onSetDateTime(getTrueDate(it, dateTime.toLocalTime())) -// } - } - - Spacer(Modifier.weight(1f)) - - IvyOutlinedButton( - text = dateTime.formatLocalTime(), - iconStart = R.drawable.ic_date - ) { -// ivyContext.timePicker { -// onSetDateTime(getTrueDate(dateTime.convertUTCtoLocal().toLocalDate(), it)) -// } - } - - Spacer(Modifier.width(24.dp)) - } -} - -private fun save( - loanRecord: LoanRecord?, - noteTextFieldValue: TextFieldValue, - amount: Double, - dateTime: LocalDateTime, - loanRecordInterest: Boolean = false, - createLoanRecordTransaction: Boolean = false, - selectedAccount: AccountOld? = null, - reCalculateAmount: Boolean = false, - - onCreate: (CreateLoanRecordData) -> Unit, - onEdit: (EditLoanRecordData) -> Unit, - dismiss: () -> Unit -) { - if (loanRecord != null) { - val record = loanRecord.copy( - note = noteTextFieldValue.text.trim(), - amount = amount, - dateTime = dateTime, - interest = loanRecordInterest, - accountId = selectedAccount?.id - ) - onEdit( - EditLoanRecordData( - newLoanRecord = record, - originalLoanRecord = loanRecord, - createLoanRecordTransaction = createLoanRecordTransaction, - reCalculateLoanAmount = reCalculateAmount, - ) - ) - } else { - onCreate( - CreateLoanRecordData( - note = noteTextFieldValue.text.trim(), - amount = amount, - dateTime = dateTime, - interest = loanRecordInterest, - account = selectedAccount, - createLoanRecordTransaction = createLoanRecordTransaction - ) - ) - } - - dismiss() -} - -@Composable -private fun AccountsRow( - modifier: Modifier = Modifier, - accounts: List, - selectedAccount: AccountOld?, - childrenTestTag: String? = null, - onSelectedAccountChanged: (AccountOld) -> Unit, - onAddNewAccount: () -> Unit -) { - val lazyState = rememberLazyListState() - - LaunchedEffect(accounts, selectedAccount) { - if (selectedAccount != null) { - val selectedIndex = accounts.indexOf(selectedAccount) - if (selectedIndex != -1) { - launch { -// if (TestingContext.inTest) return@launch //breaks UI tests - - lazyState.scrollToItem( - index = selectedIndex, //+1 because Spacer width 24.dp - ) - } - } - } - } - -// if (TestingContext.inTest) return //fix broken tests - - LazyRow( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - state = lazyState - ) { - item { - Spacer(Modifier.width(24.dp)) - } - - itemsIndexed(accounts) { _, account -> - Account( - account = account, - selected = selectedAccount == account, - testTag = childrenTestTag ?: "account" - ) { - onSelectedAccountChanged(account) - } - } - - item { - AddAccount { - onAddNewAccount() - } - } - - item { - Spacer(Modifier.width(24.dp)) - } - } -} - -@Composable -private fun Account( - account: AccountOld, - selected: Boolean, - testTag: String, - onClick: () -> Unit -) { - val accountColor = account.color.toComposeColor() - val textColor = - if (selected) findContrastTextColor(accountColor) else UI.colorsInverted.pure - - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .thenIf(!selected) { - border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - } - .thenIf(selected) { - background(accountColor, UI.shapes.fullyRounded) - } - .clickable(onClick = onClick) - .testTag(testTag), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - ItemIconSDefaultIcon( - iconName = account.icon, - defaultIcon = R.drawable.ic_custom_account_s, - tint = textColor - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier.padding(vertical = 10.dp), - text = account.name, - style = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.width(8.dp)) -} - -@Composable -private fun AddAccount( - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - .clickable(onClick = onClick), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - IvyIcon( - icon = R.drawable.ic_plus, - tint = UI.colorsInverted.pure - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier.padding(vertical = 10.dp), - text = stringResource(R.string.add_account), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.width(8.dp)) -} - - -@Preview -@Composable -private fun Preview() { - IvyPreview { - LoanRecordModal( - modal = LoanRecordModalData( - loanRecord = null, - baseCurrency = "BGN" - ), - onCreate = {}, - onEdit = {}, - onDelete = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/MonthPickerModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/MonthPickerModal.kt deleted file mode 100644 index ce20348a1f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/MonthPickerModal.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.Month -import com.ivy.core.ui.temp.trash.Month.Companion.monthsList -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.components.WrapContentRow -import com.ivy.wallet.ui.theme.findContrastTextColor -import com.ivy.wallet.utils.dateNowUTC -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.thenIf -import java.time.LocalDate -import java.util.* - -@Composable -fun BoxWithConstraintsScope.MonthPickerModal( - id: UUID = UUID.randomUUID(), - initialDate: LocalDate, - visible: Boolean, - dismiss: () -> Unit, - onMonthSelected: (Int) -> Unit -) { - var selectedMonth by remember { - mutableStateOf(initialDate.monthValue) - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSave { - onMonthSelected(selectedMonth) - dismiss() - } - } - ) { - val view = LocalView.current - - Spacer(Modifier.height(32.dp)) - - ModalTitle( - text = stringResource(R.string.choose_month) - ) - - Spacer(Modifier.height(24.dp)) - - MonthPicker( - selectedMonth = selectedMonth, - onMonthSelected = { - selectedMonth = it - } - ) - - Spacer(Modifier.height(56.dp)) - } -} - -@Composable -private fun MonthPicker( - selectedMonth: Int, - onMonthSelected: (Int) -> Unit -) { - val months = monthsList() - - WrapContentRow( - modifier = Modifier - .padding(horizontal = 16.dp), - horizontalMarginBetweenItems = 12.dp, - verticalMarginBetweenRows = 12.dp, - items = months - ) { - MonthButton( - month = it, - selected = it.monthValue == selectedMonth - ) { - onMonthSelected(it.monthValue) - } - } -} - - -@Composable -private fun MonthButton( - month: Month, - selected: Boolean, - onClick: () -> Unit, -) { - val monthColor = Ivy - - Text( - modifier = Modifier - .thenIf(selected) { - drawColoredShadow(monthColor) - } - .clip(UI.shapes.fullyRounded) - .clickable(onClick = onClick) - .thenIf(!selected) { - border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - } - .thenIf(selected) { - background( - brush = Gradient - .solid(monthColor) - .asHorizontalBrush(), - UI.shapes.fullyRounded - ) - } - .padding(horizontal = 40.dp, vertical = 12.dp), - text = month.name, - style = UI.typo.b2.style( - color = if (selected) - findContrastTextColor(monthColor) else UI.colorsInverted.pure, - fontWeight = FontWeight.SemiBold - ) - ) -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - MonthPickerModal( - initialDate = dateNowUTC(), - visible = true, - dismiss = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/NameModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/NameModal.kt deleted file mode 100644 index ece45ab6a5..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/NameModal.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.components.IvyTitleTextField -import com.ivy.wallet.utils.selectEndTextFieldValue -import java.util.* - -@Composable -fun BoxWithConstraintsScope.NameModal( - visible: Boolean, - name: String, - dismiss: () -> Unit, - id: UUID = UUID.randomUUID(), - onNameChanged: (String) -> Unit -) { - var modalName by remember(id) { mutableStateOf(selectEndTextFieldValue(name)) } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSave { - onNameChanged(modalName.text) - dismiss() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.edit_name), - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.height(32.dp)) - - IvyTitleTextField( - modifier = Modifier.padding(horizontal = 32.dp), - dividerModifier = Modifier.padding(horizontal = 24.dp), - value = modalName, - hint = stringResource(R.string.what_is_your_name) - ) { - modalName = it - } - - Spacer(Modifier.height(48.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - NameModal( - visible = true, - name = "Iliyan", - dismiss = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ProgressModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/ProgressModal.kt deleted file mode 100644 index 90dcdb3ff3..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/ProgressModal.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.* -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.Red -import java.util.* - -@Composable -fun BoxWithConstraintsScope.ProgressModal( - id: UUID = UUID.randomUUID(), - title: String, - description: String, - visible: Boolean, - color: Color = UI.colors.orange, - dismiss: () -> Unit = {}, -) { - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = {} - ) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = title, - style = UI.typo.b1.style( - color = Red, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(horizontal = 32.dp), - text = description, - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Medium - ) - ) - - Spacer(Modifier.height(24.dp)) - - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp) - .height(8.dp) - .clip( - UI.shapes.fullyRounded - ), - color = color - ) - - Spacer(Modifier.height(48.dp)) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/RecurringRuleModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/RecurringRuleModal.kt deleted file mode 100644 index a7de7cc8fd..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/RecurringRuleModal.kt +++ /dev/null @@ -1,393 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.data.planned.IntervalType -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.GradientIvy -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.White -import com.ivy.wallet.ui.theme.components.IntervalPickerRow -import com.ivy.wallet.ui.theme.components.IvyCircleButton -import com.ivy.wallet.ui.theme.components.IvyDividerLine -import com.ivy.wallet.utils.* -import java.time.LocalDate -import java.time.LocalDateTime -import java.util.* - -data class RecurringRuleModalData( - val initialStartDate: LocalDateTime?, - val initialIntervalN: Int?, - val initialIntervalType: IntervalType?, - val initialOneTime: Boolean = false, - val id: UUID = UUID.randomUUID() -) - -@Composable -fun BoxWithConstraintsScope.RecurringRuleModal( - modal: RecurringRuleModalData?, - - dismiss: () -> Unit, - onRuleChanged: (LocalDateTime, oneTime: Boolean, Int?, IntervalType?) -> Unit, -) { - var startDate by remember(modal) { - mutableStateOf(modal?.initialStartDate ?: timeNowUTC()) - } - var oneTime by remember(modal) { - mutableStateOf(modal?.initialOneTime ?: false) - } - var intervalN by remember(modal) { - mutableStateOf(modal?.initialIntervalN ?: 1) - } - var intervalType by remember(modal) { - mutableStateOf(modal?.initialIntervalType ?: IntervalType.MONTH) - } - - val modalScrollState = rememberScrollState() - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - scrollState = modalScrollState, - PrimaryAction = { - ModalSet( - modifier = Modifier.testTag("recurringModalSet"), - enabled = validate(oneTime, intervalN, intervalType) - ) { - dismiss() - onRuleChanged( - startDate, - oneTime, - intervalN, - intervalType - ) - } - } - ) { - Spacer(Modifier.height(32.dp)) - - val rootView = LocalView.current - - ModalTitle(text = stringResource(R.string.plan_for)) - - Spacer(Modifier.height(16.dp)) - - //One-time & Multiple Times - TimesSelector(oneTime = oneTime) { - oneTime = it - } - - if (oneTime) { - OneTime( - date = startDate, - onDatePicked = { - startDate = it - } - ) - } else { - MultipleTimes( - startDate = startDate, - intervalN = intervalN, - intervalType = intervalType, - - modalScrollState = modalScrollState, - - onSetStartDate = { - startDate = it - }, - onSetIntervalN = { - intervalN = it - }, - onSetIntervalType = { - intervalType = it - } - ) - } - } -} - -private fun validate( - oneTime: Boolean, - intervalN: Int?, - intervalType: IntervalType? -): Boolean { - return oneTime || intervalN != null && intervalN > 0 && intervalType != null -} - -@Composable -private fun TimesSelector( - oneTime: Boolean, - - onSetOneTime: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .padding(horizontal = 24.dp) - .fillMaxWidth() - .background(UI.colors.medium, UI.shapes.rounded), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(8.dp)) - - TimesSelectorButton( - selected = oneTime, - label = stringResource(R.string.one_time) - ) { - onSetOneTime(true) - } - - Spacer(Modifier.width(8.dp)) - - TimesSelectorButton( - selected = !oneTime, - label = stringResource(R.string.multiple_times) - ) { - onSetOneTime(false) - } - - Spacer(Modifier.width(8.dp)) - } -} - -@Composable -private fun RowScope.TimesSelectorButton( - selected: Boolean, - label: String, - onClick: () -> Unit -) { - Text( - modifier = Modifier - .weight(1f) - .clip(UI.shapes.fullyRounded) - .clickable { - onClick() - } - .padding(vertical = 8.dp) - .thenIf(selected) { - background(GradientIvy.asHorizontalBrush(), UI.shapes.fullyRounded) - } - .padding(vertical = 8.dp), - text = label, - style = UI.typo.b2.style( - color = if (selected) White else Gray, - fontWeight = FontWeight.ExtraBold, - textAlign = TextAlign.Center - ) - ) -} - -@Composable -private fun OneTime( - date: LocalDateTime, - onDatePicked: (LocalDateTime) -> Unit -) { - Spacer(Modifier.height(44.dp)) - - DateRow(dateTime = date) { - onDatePicked(it) - } - - Spacer(Modifier.height(64.dp)) -} - -@Composable -private fun MultipleTimes( - startDate: LocalDateTime, - intervalN: Int, - intervalType: IntervalType, - - modalScrollState: ScrollState, - - onSetStartDate: (LocalDateTime) -> Unit, - onSetIntervalN: (Int) -> Unit, - onSetIntervalType: (IntervalType) -> Unit -) { - Spacer(Modifier.height(40.dp)) - - Text( - modifier = Modifier - .padding(start = 32.dp), - text = stringResource(R.string.starts_on), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(12.dp)) - - DateRow(dateTime = startDate) { - onSetStartDate(it) - } - - Spacer(Modifier.height(32.dp)) - - IvyDividerLine( - modifier = Modifier.padding(horizontal = 24.dp) - ) - - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier - .padding(start = 32.dp), - text = stringResource(R.string.repeats_every_text), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.height(16.dp)) - - val rootView = LocalView.current - val coroutineScope = rememberCoroutineScope() - - IntervalPickerRow( - intervalN = intervalN, - intervalType = intervalType, - onSetIntervalN = onSetIntervalN, - onSetIntervalType = onSetIntervalType - ) - - Spacer(Modifier.height(48.dp)) -} - -@Composable -private fun DateRow( - dateTime: LocalDateTime, - onDatePicked: (LocalDateTime) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(32.dp)) - -// val ivyContext = com.ivy.core.ui.temp.ivyWalletCtx() - - Column( - modifier = Modifier.clickableNoIndication { -// ivyContext.pickDate(dateTime.toLocalDate(), onDatePicked) - } - ) { - val date = dateTime.toLocalDate() - val closeDay = date.closeDay() - - Text( - text = closeDay ?: date.formatNicely( - pattern = "EEEE, dd MMM" - ), - style = UI.typo.h2.style( - fontWeight = FontWeight.Normal, - color = UI.colorsInverted.pure - ) - ) - - if (closeDay != null) { - Spacer(Modifier.height(4.dp)) - - Text( - text = date.formatDateWeekDayLong(), - style = UI.typo.b2.style( - fontWeight = FontWeight.SemiBold, - color = Gray - ) - ) - } - } - - Spacer(Modifier.width(24.dp)) - Spacer(Modifier.weight(1f)) - - IvyCircleButton( - modifier = Modifier - .size(48.dp) - .testTag("recurring_modal_pick_date"), - backgroundPadding = 4.dp, - icon = R.drawable.ic_calendar, - backgroundGradient = Gradient.solid(UI.colorsInverted.pure), - tint = UI.colors.pure - ) { -// ivyContext.pickDate(dateTime.toLocalDate(), onDatePicked) - } - - Spacer(Modifier.width(32.dp)) - } -} - -private fun IvyWalletCtx.pickDate( - initialDate: LocalDate, - onDatePicked: ( - LocalDateTime - ) -> Unit -) { - datePicker( - initialDate = initialDate - ) { - onDatePicked(it.atTime(12, 0)) - } -} - - -@Preview -@Composable -private fun Preview_oneTime() { - IvyPreview { - BoxWithConstraints(Modifier.padding(bottom = 48.dp)) { - - RecurringRuleModal( - modal = RecurringRuleModalData( - initialStartDate = null, - initialIntervalN = null, - initialIntervalType = null, - initialOneTime = true - ), - dismiss = {}, - onRuleChanged = { _, _, _, _ -> } - ) - } - } -} - -@Preview -@Composable -private fun Preview_multipleTimes() { - IvyPreview { - BoxWithConstraints(Modifier.padding(bottom = 48.dp)) { - RecurringRuleModal( - modal = RecurringRuleModalData( - initialStartDate = null, - initialIntervalN = null, - initialIntervalType = null, - initialOneTime = false - ), - dismiss = {}, - onRuleChanged = { _, _, _, _ -> } - ) - } - - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/RequestFeatureModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/RequestFeatureModal.kt deleted file mode 100644 index 76bca42376..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/RequestFeatureModal.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextRange -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.IvyDescriptionTextField -import com.ivy.wallet.utils.selectEndTextFieldValue -import java.util.* - -@Composable -fun BoxWithConstraintsScope.RequestFeatureModal( - id: UUID = UUID.randomUUID(), - visible: Boolean, - - dismiss: () -> Unit, - onSubmit: (title: String, body: String) -> Unit -) { - var title by remember(id) { - mutableStateOf(selectEndTextFieldValue("")) - } - var body by remember(id) { - mutableStateOf(selectEndTextFieldValue("")) - } - - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSet( - label = stringResource(R.string.submit), - enabled = title.text.isNotBlank() - ) { - onSubmit( - title.text, - body.text - ) - dismiss() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle(text = stringResource(R.string.request_a_feature)) - - Spacer(Modifier.height(24.dp)) - - ModalNameInput( - hint = stringResource(R.string.what_do_you_need), - autoFocusKeyboard = true, - textFieldValue = title, - setTextFieldValue = { - title = it - } - ) - - Spacer(Modifier.height(16.dp)) - - IvyDescriptionTextField( - modifier = Modifier - .padding(horizontal = 32.dp) - .fillMaxWidth(), - keyboardOptions = KeyboardOptions( - autoCorrect = true, - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default - ), - keyboardActions = KeyboardActions( - onAny = { - body = body.copy( - text = StringBuilder(body.text) - .insert(body.selection.end, "\n") - .toString(), - selection = TextRange(body.selection.end + 1) - ) - } - ), - hint = stringResource(R.string.explain_it_in_one_sentence), - hintColor = Gray, - value = body, - ) { - body = it - } - - Spacer(Modifier.height(24.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - RequestFeatureModal( - visible = true, - dismiss = {}, - onSubmit = { _, _ -> } - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/AccountModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/AccountModal.kt deleted file mode 100644 index 4ae9a62605..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/AccountModal.kt +++ /dev/null @@ -1,329 +0,0 @@ -package com.ivy.wallet.ui.theme.modal.edit - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -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.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.AccountOld -import com.ivy.data.IvyCurrency -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.components.IvyCheckboxWithText -import com.ivy.wallet.ui.theme.components.IvyColorPicker -import com.ivy.wallet.ui.theme.modal.* -import com.ivy.wallet.utils.isNotNullOrBlank -import com.ivy.wallet.utils.selectEndTextFieldValue -import com.ivy.wallet.utils.toLowerCaseLocal -import com.ivy.wallet.utils.toUpperCaseLocal -import java.util.* - -data class AccountModalData( - val account: AccountOld?, - val baseCurrency: String, - val balance: Double, - val adjustBalanceMode: Boolean = false, - val forceNonZeroBalance: Boolean = false, - val autoFocusKeyboard: Boolean = true, - val id: UUID = UUID.randomUUID() -) - -@Composable -fun BoxWithConstraintsScope.AccountModal( - modal: AccountModalData?, - onCreateAccount: (CreateAccountData) -> Unit, - onEditAccount: (AccountOld, balance: Double) -> Unit, - dismiss: () -> Unit, -) { - val account = modal?.account - var nameTextFieldValue by remember(modal) { - mutableStateOf(selectEndTextFieldValue(account?.name)) - } - var color by remember(modal) { - mutableStateOf(account?.color?.let { Color(it) } ?: Ivy) - } - var amount by remember(modal) { - mutableStateOf(modal?.balance ?: 0.0) - } - var currencyCode by remember(modal) { - mutableStateOf(account?.currency ?: modal?.baseCurrency ?: "") - } - var icon by remember(modal) { - mutableStateOf(account?.icon) - } - var includeInBalance by remember(modal) { - mutableStateOf(account?.includeInBalance ?: true) - } - - - var amountModalVisible by remember { mutableStateOf(false) } - var currencyModalVisible by remember { mutableStateOf(false) } - var chooseIconModalVisible by remember(modal) { - mutableStateOf(false) - } - - val forceNonZeroBalance = modal?.forceNonZeroBalance ?: false - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - shiftIfKeyboardShown = false, - PrimaryAction = { - ModalAddSave( - item = modal?.account, - enabled = nameTextFieldValue.text.isNotNullOrBlank() && (!forceNonZeroBalance || amount > 0) - ) { - save( - account = account, - nameTextFieldValue = nameTextFieldValue, - currency = currencyCode, - color = color, - icon = icon, - amount = amount, - includeInBalance = includeInBalance, - - onCreateAccount = onCreateAccount, - onEditAccount = onEditAccount, - dismiss = dismiss - ) - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle( - text = if (modal?.account != null) stringResource(R.string.edit_account) else stringResource( - R.string.new_account - ), - ) - - Spacer(Modifier.height(24.dp)) - - IconNameRow( - hint = stringResource(R.string.account_name), - defaultIcon = R.drawable.ic_custom_account_m, - color = color, - icon = icon, - - autoFocusKeyboard = modal?.autoFocusKeyboard ?: true, - - nameTextFieldValue = nameTextFieldValue, - setNameTextFieldValue = { nameTextFieldValue = it }, - showChooseIconModal = { - chooseIconModalVisible = true - } - ) - - Spacer(Modifier.height(24.dp)) - - IvyColorPicker( - selectedColor = color, - onColorSelected = { color = it } - ) - - Spacer(modifier = Modifier.height(40.dp)) - - ModalAmountSection( - Header = { - Spacer(Modifier.height(16.dp)) - - AccountCurrency( - currencyCode = currencyCode - ) { - currencyModalVisible = true - } - - Spacer(modifier = Modifier.height(16.dp)) - - IvyCheckboxWithText( - modifier = Modifier - .padding(start = 16.dp) - .align(Alignment.Start), - text = stringResource(R.string.include_account), - checked = includeInBalance - ) { - includeInBalance = it - } - }, - label = stringResource(R.string.enter_account_balance).uppercase(), - currency = currencyCode, - amount = amount, - amountPaddingTop = 40.dp, - amountPaddingBottom = 40.dp, - ) { - amountModalVisible = true - } - } - - val amountModalId = remember(modal, amount) { - UUID.randomUUID() - } - AmountModal( - id = amountModalId, - visible = amountModalVisible, - currency = currencyCode, - initialAmount = amount, - dismiss = { amountModalVisible = false } - ) { newAmount -> - amount = newAmount - - if (modal?.adjustBalanceMode == true) { - save( - account = account, - nameTextFieldValue = nameTextFieldValue, - currency = currencyCode, - color = color, - icon = icon, - amount = newAmount, - includeInBalance = includeInBalance, - - onCreateAccount = onCreateAccount, - onEditAccount = onEditAccount, - dismiss = dismiss - ) - } - } - - val context = LocalContext.current - CurrencyModal( - title = stringResource(R.string.choose_currency), - initialCurrency = IvyCurrency.fromCode(currencyCode), - visible = currencyModalVisible, - dismiss = { currencyModalVisible = false } - ) { - currencyCode = it - -// if (IvyCurrency.fromCode(it)?.isCrypto == true) { -// if (getCustomIconId(context = context, iconName = it, size = "m") != null) { -// icon = it -// } -// } - } - - ChooseIconModal( - visible = chooseIconModalVisible, - initialIcon = icon ?: "account", - color = color, - dismiss = { chooseIconModalVisible = false } - ) { - icon = it - } -} - -private fun save( - account: AccountOld?, - nameTextFieldValue: TextFieldValue, - currency: String, - color: Color, - icon: String?, - amount: Double, - includeInBalance: Boolean, - - onCreateAccount: (CreateAccountData) -> Unit, - onEditAccount: (AccountOld, balance: Double) -> Unit, - dismiss: () -> Unit -) { - if (account != null) { - onEditAccount( - account.copy( - name = nameTextFieldValue.text.trim(), - currency = currency, - includeInBalance = includeInBalance, - icon = icon, - color = color.toArgb() - ), - amount - ) - } else { - onCreateAccount( - CreateAccountData( - name = nameTextFieldValue.text.trim(), - currency = currency, - color = color, - icon = icon, - balance = amount, - includeBalance = includeInBalance - ) - ) - } - - dismiss() -} - -@Composable -private fun AccountCurrency( - currencyCode: String, - - onClick: () -> Unit -) { - Row( - modifier = Modifier - .padding(horizontal = 16.dp) - .background(UI.colors.medium, UI.shapes.squared) - .clip(UI.shapes.squared) - .clickable { - onClick() - } - .padding(vertical = 24.dp) - .testTag("account_modal_currency"), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(32.dp)) - - Text( - text = currencyCode.toUpperCaseLocal(), - style = UI.typo.b1.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.weight(1f)) - - val currencyName = IvyCurrency.fromCode(currencyCode)?.name ?: "" - Text( - text = "-${currencyName}".toLowerCaseLocal(), - style = UI.typo.b2.style( - fontWeight = FontWeight.SemiBold, - color = Gray - ) - ) - - Spacer(Modifier.width(24.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - AccountModal( - modal = AccountModalData( - account = null, - baseCurrency = "BGN", - balance = 0.0 - ), - onCreateAccount = { }, - onEditAccount = { _, _ -> }) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/AmountModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/AmountModal.kt deleted file mode 100644 index 3293f044ce..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/AmountModal.kt +++ /dev/null @@ -1,499 +0,0 @@ -package com.ivy.wallet.ui.theme.modal.edit - -import android.annotation.SuppressLint -import android.content.Context -import android.os.VibrationEffect -import android.os.Vibrator -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.* -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.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.IvyCurrency -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.ui.theme.Red -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.ui.theme.modal.ModalPositiveButton -import com.ivy.wallet.ui.theme.modal.modalPreviewActionRowHeight -import com.ivy.wallet.utils.* -import java.util.* -import kotlin.math.truncate - -@Composable -fun BoxWithConstraintsScope.AmountModal( - id: UUID, - visible: Boolean, - currency: String, - initialAmount: Double?, - decimalCountMax: Int = 2, - Header: (@Composable () -> Unit)? = null, - amountSpacerTop: Dp = 64.dp, - dismiss: () -> Unit, - onAmountChanged: (Double) -> Unit, -) { - var amount by remember(id) { - mutableStateOf( - if (currency.isNotEmpty()) - initialAmount?.takeIf { it != 0.0 }?.format(currency) - ?: "" - else - initialAmount?.takeIf { it != 0.0 }?.format(decimalCountMax) - ?: "" - ) - } - - var calculatorModalVisible by remember(id) { - mutableStateOf(false) - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - IvyIcon( - modifier = circleButtonModifier( - size = 52.dp, - onClick = { - calculatorModalVisible = true - }) - .testTag("btn_calculator") - .padding(all = 4.dp), - icon = R.drawable.ic_custom_calculator_m, - tint = UI.colorsInverted.pure - ) - - Spacer(Modifier.width(16.dp)) - - ModalPositiveButton( - text = stringResource(R.string.enter), - iconStart = R.drawable.ic_check - ) { - try { - if (amount.isEmpty()) { - onAmountChanged(0.0) - } else { - onAmountChanged(amount.amountToDouble()) - } - dismiss() - } catch (e: Exception) { - e.printStackTrace() - } - } - } - ) { - Header?.invoke() - - Spacer(Modifier.height(amountSpacerTop)) - - val rootView = LocalView.current - - AmountCurrency( - amount = amount, - currency = currency - ) - - Spacer(Modifier.height(56.dp)) - - AmountInput( - currency = currency, - decimalCountMax = decimalCountMax, - amount = amount - ) { - amount = it - } - - Spacer(Modifier.height(24.dp)) - } - - CalculatorModal( - visible = calculatorModalVisible, - initialAmount = amount.amountToDoubleOrNull(), - currency = currency, - dismiss = { - calculatorModalVisible = false - }, - onCalculation = { - amount = if (currency.isNotEmpty()) it.format(currency) else it.format(decimalCountMax) - } - ) -} - -@Composable -fun AmountCurrency( - amount: String, - currency: String -) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.weight(1f)) - - Text( - text = amount.ifBlank { "0" }, - style = UI.typoSecond.h1.style( - fontWeight = FontWeight.Bold, - color = UI.colorsInverted.pure - ) - ) - Spacer(Modifier.width(4.dp)) - Text( - text = currency, - style = UI.typoSecond.h2.style( - fontWeight = FontWeight.Normal, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.weight(1f)) - } -} - -@Composable -fun AmountInput( - currency: String, - amount: String, - decimalCountMax: Int = 2, - setAmount: (String) -> Unit, - - ) { - var firstInput by remember { mutableStateOf(true) } - - AmountKeyboard( - forCalculator = false, - onNumberPressed = { - if (firstInput) { - setAmount(it) - firstInput = false - } else { - val formattedAmount = formatInputAmount( - currency = currency, - amount = amount, - newSymbol = it, - decimalCountMax = decimalCountMax - ) - if (formattedAmount != null) { - setAmount(formattedAmount) - } - } - }, - onDecimalPoint = { - if (firstInput) { - setAmount("0${localDecimalSeparator()}") - firstInput = false - } else { - val newlyEnteredString = if (amount.isEmpty()) - "0${localDecimalSeparator()}" else "$amount${localDecimalSeparator()}" - if (newlyEnteredString.amountToDoubleOrNull() != null) { - setAmount(newlyEnteredString) - } - } - - }, - onBackspace = { - if (firstInput) { - setAmount("") - firstInput = false - } else { - if (amount.isNotEmpty()) { - val formattedNumber = formatNumber(amount.dropLast(1), currency) - setAmount(formattedNumber ?: "") - } - } - } - ) -} - -private fun formatNumber(number: String, currency: String): String? { - val decimalPartString = number - .split(localDecimalSeparator()) - .getOrNull(1) - val newDecimalCount = decimalPartString?.length ?: 0 - - val amountDouble = number.amountToDoubleOrNull() - - if ((newDecimalCount <= 2 || IvyCurrency.fromCode(currency)?.isCrypto == true) && - amountDouble != null - ) { - val intPart = truncate(amountDouble).toInt() - val decimalFormatted = if (decimalPartString != null) { - "${localDecimalSeparator()}${decimalPartString}" - } else "" - - return formatInt(intPart) + decimalFormatted - } - - return null -} - -@Composable -fun AmountKeyboard( - forCalculator: Boolean, - ZeroRow: (@Composable RowScope.() -> Unit)? = null, - FirstRowExtra: (@Composable RowScope.() -> Unit)? = null, - SecondRowExtra: (@Composable RowScope.() -> Unit)? = null, - ThirdRowExtra: (@Composable RowScope.() -> Unit)? = null, - FourthRowExtra: (@Composable RowScope.() -> Unit)? = null, - onNumberPressed: (String) -> Unit, - onDecimalPoint: () -> Unit, - onBackspace: () -> Unit, -) { - if (ZeroRow != null) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - ZeroRow.invoke(this) - } - - Spacer(Modifier.height(16.dp)) - } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CircleNumberButton( - forCalculator = forCalculator, - value = "7", - onNumberPressed = onNumberPressed - ) - - Spacer(Modifier.width(16.dp)) - - CircleNumberButton( - forCalculator = forCalculator, - value = "8", - onNumberPressed = onNumberPressed - ) - - Spacer(Modifier.width(16.dp)) - - CircleNumberButton( - forCalculator = forCalculator, - value = "9", - onNumberPressed = onNumberPressed - ) - - if (FirstRowExtra != null) { - Spacer(modifier = Modifier.width(16.dp)) - - FirstRowExtra.invoke(this) - } - } - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CircleNumberButton( - forCalculator = forCalculator, - value = "4", - onNumberPressed = onNumberPressed - ) - - Spacer(Modifier.width(16.dp)) - - CircleNumberButton( - forCalculator = forCalculator, - value = "5", - onNumberPressed = onNumberPressed - ) - - Spacer(Modifier.width(16.dp)) - - CircleNumberButton( - forCalculator = forCalculator, - value = "6", - onNumberPressed = onNumberPressed - ) - - if (SecondRowExtra != null) { - Spacer(modifier = Modifier.width(16.dp)) - - SecondRowExtra.invoke(this) - } - } - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CircleNumberButton( - forCalculator = forCalculator, - value = "1", - onNumberPressed = onNumberPressed - ) - - Spacer(Modifier.width(16.dp)) - - CircleNumberButton( - forCalculator = forCalculator, - value = "2", - onNumberPressed = onNumberPressed - ) - - Spacer(Modifier.width(16.dp)) - - CircleNumberButton( - forCalculator = forCalculator, - value = "3", - onNumberPressed = onNumberPressed - ) - - if (ThirdRowExtra != null) { - Spacer(modifier = Modifier.width(16.dp)) - - ThirdRowExtra.invoke(this) - } - } - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - KeypadCircleButton( - text = localDecimalSeparator(), - testTag = if (forCalculator) - "calc_key_decimal_separator" else "key_decimal_separator" - ) { - onDecimalPoint() - } - - Spacer(Modifier.width(16.dp)) - - CircleNumberButton( - forCalculator = forCalculator, - value = "0", - onNumberPressed = onNumberPressed - ) - - Spacer(Modifier.width(16.dp)) - - IvyIcon( - modifier = circleButtonModifier(onClick = onBackspace, hapticFeedback = true) - .padding(all = 16.dp) - .testTag("key_del"), - icon = R.drawable.ic_backspace, - tint = Red - ) - - if (FourthRowExtra != null) { - Spacer(modifier = Modifier.width(16.dp)) - - FourthRowExtra.invoke(this) - } - } -} - -@Composable -fun CircleNumberButton( - forCalculator: Boolean, - value: String, - onNumberPressed: (String) -> Unit, -) { - KeypadCircleButton( - text = value, - testTag = if (forCalculator) - "calc_key_${value}" else "key_${value}", - onClick = { - onNumberPressed(value) - } - ) -} - -@Composable -fun KeypadCircleButton( - text: String, - textColor: Color = UI.colorsInverted.pure, - testTag: String, - onClick: () -> Unit -) { - Text( - modifier = circleButtonModifier(onClick = onClick, hapticFeedback = true) - .padding(top = 10.dp) - .testTag(testTag), - text = text, - style = UI.typoSecond.h2.style( - color = textColor, - fontWeight = FontWeight.Bold - ).copy( - textAlign = TextAlign.Center - ) - ) -} - -@SuppressLint("ComposableModifierFactory", "ModifierFactoryExtensionFunction", "MissingPermission") -@Composable -private fun circleButtonModifier( - size: Dp = 64.dp, - hapticFeedback: Boolean = false, - onClick: () -> Unit -): Modifier { - val context = LocalContext.current - val vibrator = remember {context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator} - - return Modifier - .size(size) - .clip(CircleShape) - .clickable { - onClick() - if (hapticFeedback) { - vibrator.vibrate(VibrationEffect.createOneShot(100, 1)) - } - } - .background(UI.colors.pure, UI.shapes.fullyRounded) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - BoxWithConstraints( - modifier = Modifier.padding(bottom = modalPreviewActionRowHeight()) - ) { - AmountModal( - id = UUID.randomUUID(), - visible = true, - currency = "BGN", - initialAmount = null, - dismiss = { } - ) { - - } - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/CalculatorModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/CalculatorModal.kt deleted file mode 100644 index 2c8c311b96..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/CalculatorModal.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.ivy.wallet.ui.theme.modal.edit - -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.Red -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.ui.theme.modal.ModalSet -import com.ivy.wallet.ui.theme.modal.ModalTitle -import com.ivy.wallet.utils.* -import com.notkamui.keval.Keval -import java.util.* - -@Composable -fun BoxWithConstraintsScope.CalculatorModal( - id: UUID = UUID.randomUUID(), - initialAmount: Double?, - visible: Boolean, - currency: String, - - dismiss: () -> Unit, - onCalculation: (Double) -> Unit -) { - var expression by remember(id, initialAmount) { - mutableStateOf(initialAmount?.format(currency) ?: "") - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSet( - modifier = Modifier.testTag("calc_set") - ) { - val result = calculate(expression) - if (result != null) { - onCalculation(result) - dismiss() - } - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle(text = stringResource(R.string.calculator)) - - Spacer(Modifier.height(32.dp)) - - val isEmpty = expression.isBlank() - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - text = if (isEmpty) stringResource(R.string.calculator_empty_expression) else expression, - style = UI.typoSecond.h2.style( - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - color = if (isEmpty) Gray else UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.height(32.dp)) - - AmountKeyboard( - forCalculator = true, - ZeroRow = { - KeypadCircleButton( - text = "C", - textColor = Red, - testTag = "key_C" - ) { - expression = "" - } - - Spacer(Modifier.width(16.dp)) - - KeypadCircleButton( - text = "(", - testTag = "key_(" - ) { - expression += "(" - } - - Spacer(Modifier.width(16.dp)) - - KeypadCircleButton( - text = ")", - testTag = "key_)" - ) { - expression += ")" - } - - Spacer(Modifier.width(16.dp)) - - KeypadCircleButton( - text = "/", - testTag = "key_/" - ) { - expression += "/" - } - }, - FirstRowExtra = { - KeypadCircleButton( - text = "*", - testTag = "key_*" - ) { - expression += "*" - } - }, - SecondRowExtra = { - KeypadCircleButton( - text = "-", - testTag = "key_-" - ) { - expression += "-" - } - }, - ThirdRowExtra = { - KeypadCircleButton( - text = "+", - testTag = "key_+" - ) { - expression += "+" - } - }, - FourthRowExtra = { - KeypadCircleButton( - text = "=", - testTag = "key_=" - ) { - val result = calculate(expression) - if (result != null) { - expression = result.format(currency) - } - } - }, - - onNumberPressed = { - expression = formatExpression( - expression = expression + it, - ) - }, - onDecimalPoint = { - expression = formatExpression( - expression = expression + localDecimalSeparator(), - ) - }, - onBackspace = { - if (expression.isNotEmpty()) { - expression = expression.dropLast(1) - } - } - ) - - Spacer(Modifier.height(24.dp)) - } -} - -private fun formatExpression(expression: String): String { - var formattedExpression = expression - - expression - .split("(", ")", "/", "*", "-", "+") - .ifEmpty { - //handle only number expression formatting - listOf(expression) - } - .forEach { part -> - val formattedPart = removeExtraDecimals(part) - - val numberPart = formattedPart - .amountToDoubleOrNull() - if (numberPart != null) { - formattedExpression = formattedExpression.replace(part, formattedPart) - } - } - - return formattedExpression -} - -private fun calculate(expression: String): Double? { - return try { - Keval.eval(expression.normalizeExpression()) - } catch (e: Exception) { - null - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - CalculatorModal( - visible = true, - initialAmount = 50.23, - currency = "BGN", - dismiss = { }, - onCalculation = {} - ) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/CategoryModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/CategoryModal.kt deleted file mode 100644 index 901d36e8e9..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/CategoryModal.kt +++ /dev/null @@ -1,285 +0,0 @@ -package com.ivy.wallet.ui.theme.modal.edit - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.CategoryOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview - -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.ui.category.CategoryList -import com.ivy.wallet.ui.theme.Ivy -import com.ivy.wallet.ui.theme.components.ItemIconMDefaultIcon -import com.ivy.wallet.ui.theme.components.IvyCheckboxWithText -import com.ivy.wallet.ui.theme.components.IvyColorPicker -import com.ivy.wallet.ui.theme.components.IvyNameTextField -import com.ivy.wallet.ui.theme.dynamicContrast -import com.ivy.wallet.ui.theme.modal.ChooseIconModal -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.ui.theme.modal.ModalAddSave -import com.ivy.wallet.ui.theme.modal.ModalTitle -import com.ivy.wallet.utils.hideKeyboard -import com.ivy.wallet.utils.isNotNullOrBlank -import com.ivy.wallet.utils.selectEndTextFieldValue -import java.util.* - -data class CategoryModalData( - val category: CategoryOld?, - val id: UUID = UUID.randomUUID(), - val autoFocusKeyboard: Boolean = true -) - -@Composable -fun BoxWithConstraintsScope.CategoryModal( - modal: CategoryModalData?, - isCategoryParentCategory: Boolean = true, - parentCategoryList: List = emptyList(), - onCreateCategory: (CreateCategoryData) -> Unit, - onEditCategory: (CategoryOld) -> Unit, - dismiss: () -> Unit, -) { - val initialCategory = modal?.category - var nameTextFieldValue by remember(modal) { - mutableStateOf(selectEndTextFieldValue(initialCategory?.name)) - } - var color by remember(modal) { - mutableStateOf(initialCategory?.color?.let { Color(it) } ?: Ivy) - } - var icon by remember(modal) { - mutableStateOf(initialCategory?.icon) - } - var chooseIconModalVisible by remember(modal) { - mutableStateOf(false) - } - - var isSubCategory by remember(modal) { - mutableStateOf(modal?.category?.parentCategoryId != null) - } - - var selectedParentCategory: CategoryOld? by remember(modal) { - mutableStateOf(parentCategoryList.find { it.id == modal?.category?.parentCategoryId }) - } - val isParentCat: Boolean by remember(modal) { - mutableStateOf(if (initialCategory == null) false else isCategoryParentCategory) - } - - IvyModal( - id = modal?.id, - visible = modal != null, - dismiss = dismiss, - PrimaryAction = { - ModalAddSave( - item = modal?.category, - enabled = nameTextFieldValue.text.isNotNullOrBlank() - && ((isSubCategory && selectedParentCategory != null) || !isSubCategory) - ) { - if (initialCategory != null) { - onEditCategory( - initialCategory.copy( - name = nameTextFieldValue.text.trim(), - color = color.toArgb(), - icon = icon, - parentCategoryId = selectedParentCategory?.id - ) - ) - } else { - onCreateCategory( - CreateCategoryData( - name = nameTextFieldValue.text.trim(), - color = color, - icon = icon, - parentCategory = selectedParentCategory - ) - ) - } - - dismiss() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle( - text = if (modal?.category != null) stringResource(R.string.edit_category) - else stringResource( - R.string.create_category - ) - ) - - Spacer(Modifier.height(24.dp)) - - IconNameRow( - hint = stringResource(R.string.category_name), - defaultIcon = R.drawable.ic_custom_category_m, - color = color, - icon = icon, - - autoFocusKeyboard = modal?.autoFocusKeyboard ?: true, - - nameTextFieldValue = nameTextFieldValue, - setNameTextFieldValue = { nameTextFieldValue = it }, - showChooseIconModal = { - chooseIconModalVisible = true - } - ) - - Spacer(Modifier.height(40.dp)) - - IvyColorPicker( - selectedColor = color, - onColorSelected = { color = it } - ) - - if (isSubCategory) { - Text( - modifier = Modifier.padding(top = 16.dp, end = 32.dp, start = 32.dp), - text = stringResource(R.string.parent_category), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - CategoryList( - categoryList = parentCategoryList, - selectedCategory = selectedParentCategory - ) { - selectedParentCategory = it - } - } - - if (!isParentCat && parentCategoryList.isNotEmpty()) { - IvyCheckboxWithText( - modifier = Modifier - .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), - text = stringResource(R.string.mark_as_sub_category), - checked = isSubCategory - ) { - isSubCategory = it - if (!isSubCategory) - selectedParentCategory = - null // Reset Sub-Category if Sub-Category Option is Unchecked - } - } - if (parentCategoryList.isNotEmpty() && isParentCat) { - Text( - modifier = Modifier.padding(top = 32.dp, start = 32.dp), - text = stringResource(R.string.marked_parent_category), - style = UI.typoSecond.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Normal - ) - ) - } - - Spacer(Modifier.height(16.dp)) - } - - ChooseIconModal( - visible = chooseIconModalVisible, - initialIcon = icon ?: "category", - color = color, - dismiss = { chooseIconModalVisible = false } - ) { - icon = it - } -} - - -@Composable -fun IconNameRow( - hint: String, - @DrawableRes defaultIcon: Int, - color: Color, - icon: String?, - - autoFocusKeyboard: Boolean, - - nameTextFieldValue: TextFieldValue, - setNameTextFieldValue: (TextFieldValue) -> Unit, - - showChooseIconModal: () -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - val nameFocus = FocusRequester() - - Spacer(Modifier.width(24.dp)) - - ItemIconMDefaultIcon( - modifier = Modifier - .clip(CircleShape) - .background(color, CircleShape) - .clickable { - showChooseIconModal() - } - .testTag("modal_item_icon"), - iconName = icon, - tint = color.dynamicContrast(), - defaultIcon = defaultIcon - ) - - val view = LocalView.current - IvyNameTextField( - modifier = Modifier - .padding(start = 28.dp, end = 36.dp) - .focusRequester(nameFocus), - underlineModifier = Modifier.padding(start = 24.dp, end = 32.dp), - value = nameTextFieldValue, - hint = hint, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Words, - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Text, - autoCorrect = true - ), - keyboardActions = KeyboardActions( - onDone = { - hideKeyboard(view) - } - ), - ) { newValue -> - setNameTextFieldValue(newValue) - } - } -} - -@Preview -@Composable -private fun PreviewCategoryModal() { - IvyPreview { - CategoryModal( - modal = CategoryModalData(null), - onCreateCategory = { }, - onEditCategory = { } - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/ChooseCategoryModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/ChooseCategoryModal.kt deleted file mode 100644 index 9bbf598a34..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/ChooseCategoryModal.kt +++ /dev/null @@ -1,275 +0,0 @@ -package com.ivy.wallet.ui.theme.modal.edit - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.CategoryOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.ItemIconSDefaultIcon -import com.ivy.wallet.ui.theme.components.IvyBorderButton -import com.ivy.wallet.ui.theme.components.IvyCircleButton -import com.ivy.wallet.ui.theme.components.WrapContentRow -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.ui.theme.modal.ModalSkip -import com.ivy.wallet.ui.theme.modal.ModalTitle -import com.ivy.wallet.utils.drawColoredShadow -import com.ivy.wallet.utils.thenIf -import java.util.* - -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.ChooseCategoryModal( - id: UUID = UUID.randomUUID(), - visible: Boolean, - initialCategory: CategoryOld?, - categories: List, - - showCategoryModal: (CategoryOld?) -> Unit, - onCategoryChanged: (CategoryOld?) -> Unit, - dismiss: () -> Unit -) { - var selectedCategory by remember(initialCategory) { - mutableStateOf(initialCategory) - } - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSkip { - save( - category = selectedCategory, - onCategoryChanged = onCategoryChanged, - dismiss = dismiss - ) - } - } - ) { - val view = LocalView.current - - Spacer(Modifier.height(32.dp)) - - ModalTitle( - text = stringResource(R.string.choose_category) - ) - - Spacer(Modifier.height(24.dp)) - - CategoryPicker( - categories = categories, - selectedCategory = selectedCategory, - showCategoryModal = showCategoryModal, - onEditCategory = { - showCategoryModal(it) - } - ) { - selectedCategory = it - save( - shouldDismissModal = it != null, - category = it, - onCategoryChanged = onCategoryChanged, - dismiss = dismiss - ) - } - - Spacer(Modifier.height(56.dp)) - } -} - -private fun save( - shouldDismissModal: Boolean = true, - - category: CategoryOld?, - onCategoryChanged: (CategoryOld?) -> Unit, - dismiss: () -> Unit -) { - onCategoryChanged(category) - if (shouldDismissModal) { - dismiss() - } -} - -@ExperimentalFoundationApi -@Composable -private fun CategoryPicker( - categories: List, - selectedCategory: CategoryOld?, - showCategoryModal: (CategoryOld?) -> Unit, - onEditCategory: (CategoryOld) -> Unit, - onSelected: (CategoryOld?) -> Unit, -) { - val data = mutableListOf() - data.addAll(categories) - data.add(AddNewCategory()) - - WrapContentRow( - modifier = Modifier - .padding(horizontal = 16.dp), - horizontalMarginBetweenItems = 12.dp, - verticalMarginBetweenRows = 12.dp, - items = data - ) { - when (it) { - is CategoryOld -> { - CategoryButton( - category = it, - selected = it == selectedCategory, - onClick = { - onSelected(it) - }, - onLongClick = { - onEditCategory(it) - }, - onDeselect = { - onSelected(null) - } - ) - } - is AddNewCategory -> { - AddNewButton { - showCategoryModal(null) - } - } - } - } -} - -@ExperimentalFoundationApi -@Composable -private fun CategoryButton( - category: CategoryOld, - selected: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit, - onDeselect: () -> Unit, -) { - val categoryColor = category.color.toComposeColor() - - Row( - modifier = Modifier - .thenIf(selected) { - drawColoredShadow(categoryColor) - } - .clip(UI.shapes.fullyRounded) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick - ) - .border( - width = 2.dp, - color = if (selected) UI.colorsInverted.pure else UI.colors.medium, - shape = UI.shapes.fullyRounded - ) - .thenIf(selected) { - background(categoryColor, UI.shapes.fullyRounded) - } - .testTag("choose_category_button"), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(if (selected) 12.dp else 8.dp)) - - ItemIconSDefaultIcon( - modifier = Modifier - .background(categoryColor, CircleShape), - iconName = category.icon, - defaultIcon = R.drawable.ic_custom_category_s, - tint = findContrastTextColor(categoryColor) - ) - - Text( - modifier = Modifier - .padding(vertical = 12.dp) - .padding( - start = if (selected) 12.dp else 12.dp, - end = if (selected) 20.dp else 24.dp - ), - text = category.name, - style = UI.typo.b2.style( - color = if (selected) - findContrastTextColor(categoryColor) else UI.colorsInverted.pure, - fontWeight = FontWeight.SemiBold - ) - ) - - if (selected) { - - val deselectBtnBackground = findContrastTextColor(categoryColor) - IvyCircleButton( - modifier = Modifier - .size(32.dp), - icon = R.drawable.ic_remove, - backgroundGradient = Gradient.solid(deselectBtnBackground), - tint = findContrastTextColor(deselectBtnBackground) - ) { - onDeselect() - } - - Spacer(Modifier.width(8.dp)) - } - } -} - -@Composable -fun AddNewButton( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - IvyBorderButton( - modifier = modifier, - text = stringResource(R.string.add_new), - backgroundGradient = Gradient.solid(UI.colorsInverted.medium), - iconStart = R.drawable.ic_plus, - textStyle = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ), - iconTint = UI.colorsInverted.pure, - padding = 10.dp, - onClick = onClick - ) -} - -private class AddNewCategory - -@ExperimentalFoundationApi -@Preview -@Composable -private fun PreviewChooseCategoryModal() { - IvyPreview { - val categories = mutableListOf( - CategoryOld("Test", color = Ivy.toArgb()), - CategoryOld("Second", color = Orange.toArgb()), - CategoryOld("Third", color = Red.toArgb()), - ) - - ChooseCategoryModal( - visible = true, - initialCategory = categories.first(), - categories = categories, - showCategoryModal = { }, - onCategoryChanged = { } - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/DescriptionModal.kt b/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/DescriptionModal.kt deleted file mode 100644 index 3013da9e9d..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/modal/edit/DescriptionModal.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.ivy.wallet.ui.theme.modal.edit - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.font.FontWeight -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.design.util.hideKeyboard - -import com.ivy.wallet.ui.theme.components.IvyDescriptionTextField -import com.ivy.wallet.ui.theme.modal.IvyModal -import com.ivy.wallet.ui.theme.modal.ModalDynamicPrimaryAction -import com.ivy.wallet.utils.clickableNoIndication -import com.ivy.wallet.utils.selectEndTextFieldValue -import java.util.* - -@Composable -fun BoxWithConstraintsScope.DescriptionModal( - id: UUID = UUID.randomUUID(), - visible: Boolean, - description: String?, - - onDescriptionChanged: (String?) -> Unit, - dismiss: () -> Unit, -) { - var descTextFieldValue by remember(description) { - mutableStateOf(selectEndTextFieldValue(description)) - } - val view = com.ivy.core.ui.temp.rootView() - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalDynamicPrimaryAction( - initialEmpty = description.isNullOrBlank(), - initialChanged = description != descTextFieldValue.text, - - testTagSave = "modal_desc_save", - testTagDelete = "modal_desc_delete", - - onSave = { - onDescriptionChanged(descTextFieldValue.text) - view.hideKeyboard() - }, - onDelete = { - onDescriptionChanged(null) - view.hideKeyboard() - }, - dismiss = dismiss - ) - } - ) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier - .padding(start = 32.dp), - text = stringResource(R.string.description), - style = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(24.dp)) - - val focus = FocusRequester() - IvyDescriptionTextField( - modifier = Modifier - .padding(horizontal = 32.dp) - .fillMaxWidth() - .focusRequester(focus), - testTag = "modal_desc_input", - keyboardOptions = KeyboardOptions( - autoCorrect = true, - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default - ), - keyboardActions = KeyboardActions( - onAny = { - descTextFieldValue = descTextFieldValue.copy( - text = StringBuilder(descTextFieldValue.text) - .insert(descTextFieldValue.selection.end, "\n") - .toString(), - selection = TextRange(descTextFieldValue.selection.end + 1) - ) - } - ), - value = descTextFieldValue, - hint = stringResource(R.string.description_text_field_hint), - ) { - descTextFieldValue = it - } - - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(24.dp) - .clickableNoIndication { - focus.requestFocus() - } - ) - } -} - -@Preview -@Composable -private fun PreviewDescriptionModal_emptyText() { - IvyPreview { - DescriptionModal( - visible = true, - description = "", - onDescriptionChanged = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/wallet/AmountCurrency.kt b/ui-components-old/src/main/java/com/ivy/old/theme/wallet/AmountCurrency.kt deleted file mode 100644 index 47106c907d..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/wallet/AmountCurrency.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.ivy.wallet.ui.theme.wallet - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.utils.format -import com.ivy.wallet.utils.shortenAmount -import com.ivy.wallet.utils.shouldShortAmount - - -@Composable -fun AmountCurrencyB2Row( - amount: Double, - currency: String, - amountFontWeight: FontWeight = FontWeight.ExtraBold, - textColor: Color = UI.colorsInverted.pure -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = amount.format(currency), - style = UI.typoSecond.b2.style( - fontWeight = amountFontWeight, - color = textColor - ) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = currency, - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.Normal, - color = textColor - ) - ) - } -} - -@Composable -fun AmountCurrencyB1Row( - amount: Double, - currency: String, - amountFontWeight: FontWeight = FontWeight.Bold, - textColor: Color = UI.colorsInverted.pure -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - AmountCurrencyB1( - amount = amount, - currency = currency, - amountFontWeight = amountFontWeight, - textColor = textColor - ) - } -} - - -@Composable -fun AmountCurrencyB1( - amount: Double, - currency: String, - amountFontWeight: FontWeight = FontWeight.Bold, - textColor: Color = UI.colorsInverted.pure, - shortenBigNumbers: Boolean = false -) { - val shortAmount = shortenBigNumbers && shouldShortAmount(amount) - - Text( - modifier = Modifier.testTag("amount_currency_b1"), - text = if (shortAmount) shortenAmount(amount) else amount.format(currency), - style = UI.typoSecond.b1.style( - fontWeight = amountFontWeight, - color = textColor - ) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = currency, - style = UI.typoSecond.b1.style( - fontWeight = FontWeight.Normal, - color = textColor - ) - ) -} - -@Composable -fun AmountCurrencyH1( - amount: Double, - currency: String, - textColor: Color = UI.colorsInverted.pure -) { - Text( - text = amount.format(currency), - style = UI.typoSecond.h1.style( - fontWeight = FontWeight.Bold, - color = textColor - ) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = currency, - style = UI.typoSecond.h2.style( - fontWeight = FontWeight.Normal, - color = textColor - ) - ) -} - -@Composable -fun AmountCurrencyH2Row( - amount: Double, - currency: String, - textColor: Color = UI.colorsInverted.pure -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = amount.format(currency), - style = UI.typoSecond.h2.style( - fontWeight = FontWeight.Bold, - color = textColor - ) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = currency, - style = UI.typo.b1.style( - fontWeight = FontWeight.Normal, - color = textColor - ) - ) - } -} - -@Composable -fun AmountCurrencyCaption( - amount: Double, - currency: String, - amountFontWeight: FontWeight = FontWeight.ExtraBold, - textColor: Color = UI.colorsInverted.pure -) { - Text( - text = amount.format(currency), - style = UI.typoSecond.c.style( - fontWeight = amountFontWeight, - color = textColor - ) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = currency, - style = UI.typoSecond.c.style( - fontWeight = FontWeight.Normal, - color = textColor - ) - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/theme/wallet/PeriodSelector.kt b/ui-components-old/src/main/java/com/ivy/old/theme/wallet/PeriodSelector.kt deleted file mode 100644 index f481300556..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/theme/wallet/PeriodSelector.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.ivy.wallet.ui.theme.wallet - - -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.core.ui.temp.trash.TimePeriod -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.components.IvyIcon - -@Composable -fun PeriodSelector( - modifier: Modifier = Modifier, - period: TimePeriod, - onPreviousMonth: () -> Unit, - onNextMonth: () -> Unit, - onShowChoosePeriodModal: () -> Unit, -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(20.dp)) - - if (period.month != null) { - IvyIcon( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .clickable { - onPreviousMonth() - } - .padding(all = 8.dp) - .rotate(-180f), - icon = R.drawable.ic_arrow_right - ) - } - - Spacer(Modifier.weight(1f)) - - Row( - modifier = Modifier - .height(48.dp) - .defaultMinSize(minWidth = 48.dp) - .clip(UI.shapes.fullyRounded) - .clickable { - onShowChoosePeriodModal() - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - IvyIcon( - icon = R.drawable.ic_calendar, - tint = UI.colorsInverted.pure - ) - - Spacer(Modifier.width(4.dp)) - - Text( - text = period.toDisplayShort(1), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.Bold - ) - ) - } - - Spacer(Modifier.weight(1f)) - - if (period.month != null) { - IvyIcon( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .clickable { - onNextMonth() - } - .padding(all = 8.dp), - icon = R.drawable.ic_arrow_right - ) - } - - Spacer(Modifier.width(20.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - ComponentPreview { - PeriodSelector( - period = TimePeriod.currentMonth( - startDayOfMonth = 1 - ), //preview - onPreviousMonth = { }, - onNextMonth = { } - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/trnedit/Category.kt b/ui-components-old/src/main/java/com/ivy/old/trnedit/Category.kt deleted file mode 100644 index 0ce5639af2..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/trnedit/Category.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.ivy.wallet.ui.edit.core - -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.CategoryOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.wallet.ui.theme.Gradient -import com.ivy.wallet.ui.theme.components.IvyBorderButton -import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.getCustomIconIdS -import com.ivy.wallet.ui.theme.findContrastTextColor -import com.ivy.wallet.ui.theme.toComposeColor - -@Composable -fun Category( - category: CategoryOld?, - onChooseCategory: () -> Unit -) { - if (category != null) { - CategoryButton(category = category) { - onChooseCategory() - } - } else { - IvyBorderButton( - modifier = Modifier.padding(start = 24.dp), - iconStart = R.drawable.ic_plus, - iconTint = UI.colorsInverted.pure, - text = stringResource(R.string.add_category) - ) { - onChooseCategory() - } - } -} - -@Composable -private fun CategoryButton( - category: CategoryOld, - onClick: () -> Unit, -) { - val contrastColor = findContrastTextColor(category.color.toComposeColor()) - IvyButton( - modifier = Modifier.padding(start = 24.dp), - text = category.name, - iconStart = getCustomIconIdS( - iconName = category.icon, - defaultIcon = R.drawable.ic_custom_category_s - ), - backgroundGradient = Gradient.from(category.color, category.color), - textStyle = UI.typo.b2.style( - color = contrastColor, - fontWeight = FontWeight.Bold - ), - iconTint = contrastColor, - hasGlow = false, - iconEnd = R.drawable.ic_onboarding_next_arrow, - wrapContentMode = true, - onClick = onClick - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/trnedit/Description.kt b/ui-components-old/src/main/java/com/ivy/old/trnedit/Description.kt deleted file mode 100644 index 9c5d626b0d..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/trnedit/Description.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.ivy.wallet.ui.edit.core - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.transaction_details.PrimaryAttributeColumn -import com.ivy.wallet.ui.theme.components.AddPrimaryAttributeButton -import com.ivy.wallet.utils.isNotNullOrBlank - -@Composable -fun Description( - description: String?, - onAddDescription: () -> Unit, - onEditDescription: (String) -> Unit -) { - if (description.isNotNullOrBlank()) { - DescriptionText( - description = description!!, - onClick = { - onEditDescription(description) - } - ) - } else { - AddPrimaryAttributeButton( - icon = R.drawable.ic_description, - text = stringResource(R.string.add_description), - onClick = onAddDescription - ) - } -} - -@Composable -private fun DescriptionText( - description: String, - onClick: () -> Unit, -) { - PrimaryAttributeColumn( - icon = R.drawable.ic_description, - title = stringResource(R.string.description), - onClick = onClick - ) { - Spacer(Modifier.height(12.dp)) - - Text( - modifier = Modifier - .clickable { - onClick() - } - .padding(horizontal = 24.dp) - .testTag("trn_description"), - text = description, - style = UI.typoSecond.b2.style( - textAlign = TextAlign.Left - ), - ) - - Spacer(Modifier.height(20.dp)) - } -} - -@Preview -@Composable -private fun PreviewDescription_Empty() { - ComponentPreview { - Description( - description = null, - onAddDescription = {} - ) { - - } - } -} - -@Preview -@Composable -private fun PreviewDescription_withText() { - ComponentPreview { - Description( - description = "This is my sample description.", - onAddDescription = {} - ) { - - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/trnedit/DueDate.kt b/ui-components-old/src/main/java/com/ivy/old/trnedit/DueDate.kt deleted file mode 100644 index 5466048071..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/trnedit/DueDate.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.ivy.wallet.ui.edit.core - - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.utils.formatDateOnly -import com.ivy.wallet.utils.timeNowUTC -import java.time.LocalDateTime - -@Composable -fun DueDate( - dueDate: LocalDateTime, - onPickDueDate: () -> Unit, -) { - DueDateCard( - dueDate = dueDate, - onClick = { - onPickDueDate() - } - ) -} - -@Composable -private fun DueDateCard( - dueDate: LocalDateTime, - onClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(UI.shapes.squared) - .background(UI.colors.medium, UI.shapes.squared) - .clickable(onClick = onClick) - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(16.dp)) - - IvyIcon(icon = R.drawable.ic_planned_payments) - - Spacer(Modifier.width(8.dp)) - - Text( - text = stringResource(R.string.planned_for), - style = UI.typo.b2.style( - fontWeight = FontWeight.ExtraBold, - color = UI.colorsInverted.pure - ) - ) - - Spacer(Modifier.weight(1f)) - - Text( - text = dueDate.toLocalDate().formatDateOnly(), - style = UI.typoSecond.b2.style( - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } -} - -@Preview -@Composable -private fun Preview_OneTime() { - ComponentPreview { - DueDate( - dueDate = timeNowUTC().plusDays(5), - ) { - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/trnedit/EditBottomSheet.kt b/ui-components-old/src/main/java/com/ivy/old/trnedit/EditBottomSheet.kt deleted file mode 100644 index 743ea6a164..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/trnedit/EditBottomSheet.kt +++ /dev/null @@ -1,807 +0,0 @@ -package com.ivy.wallet.ui.edit.core - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.ivy.base.R -import com.ivy.common.Constants -import com.ivy.data.AccountOld -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.IvyPreview -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.* -import com.ivy.wallet.ui.theme.modal.DURATION_MODAL_ANIM -import com.ivy.wallet.ui.theme.modal.ModalSave -import com.ivy.wallet.ui.theme.modal.ModalSet -import com.ivy.wallet.ui.theme.modal.edit.AmountModal -import com.ivy.wallet.utils.* -import kotlinx.coroutines.launch -import java.util.* -import kotlin.math.roundToInt - -@Composable -fun BoxWithConstraintsScope.EditBottomSheet( - initialTransactionId: UUID?, - type: TrnTypeOld, - accounts: List, - selectedAccount: AccountOld?, - toAccount: AccountOld?, - amount: Double, - currency: String, - convertedAmount: Double? = null, - convertedAmountCurrencyCode: String? = null, - - amountModalShown: Boolean, - setAmountModalShown: (Boolean) -> Unit, - ActionButton: @Composable () -> Unit, - - onAmountChanged: (Double) -> Unit, - onSelectedAccountChanged: (AccountOld) -> Unit, - onToAccountChanged: (AccountOld) -> Unit, - onAddNewAccount: () -> Unit -) { - val rootView = LocalView.current - var keyboardShown by remember { mutableStateOf(false) } - - val keyboardShownInsetDp by animateDpAsState( - targetValue = densityScope { - if (keyboardShown) keyboardOnlyWindowInsets().bottom.toDp() else 0.dp - }, - animationSpec = tween(DURATION_MODAL_ANIM) - ) - val navBarPadding by animateDpAsState( - targetValue = densityScope { - if (keyboardShown) 0.dp else navigationBarInsets().bottom.toDp() - }, - animationSpec = tween(DURATION_MODAL_ANIM) - ) - - var bottomBarHeight by remember { mutableStateOf(0) } - - var internalExpanded by remember { mutableStateOf(true) } - val expanded = internalExpanded && !keyboardShown - - val percentExpanded by animateFloatAsState( - targetValue = if (expanded) 1f else 0f, - animationSpec = springBounce() - ) - val percentCollapsed = 1f - percentExpanded - - val showConvertedAmountText by remember(convertedAmount) { - if (type == TrnTypeOld.TRANSFER && convertedAmount != null && convertedAmountCurrencyCode != null) - mutableStateOf("${convertedAmount.format(2)} $convertedAmountCurrencyCode") - else - mutableStateOf(null) - } - - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .statusBarsPadding() - .padding(top = 24.dp) -// .drawColoredShadow( -// color = UI.colorsInverted.medium, -// alpha = if (UI.colors.isLight) 0.3f else 0.2f, -// borderRadius = 24.dp, -// shadowRadius = 24.dp -// ) - .border( - width = 2.dp, - color = UI.colors.medium, - shape = UI.shapes.roundedTop - ) - .background(UI.colors.pure, UI.shapes.roundedTop) - .verticalSwipeListener( - sensitivity = Constants.SWIPE_UP_EXPANDED_THRESHOLD, - onSwipeUp = { - hideKeyboard(rootView) - internalExpanded = true - }, - onSwipeDown = { - internalExpanded = false - } - ) - .consumeClicks() - ) { - //Accounts label - val label = when (type) { - TrnTypeOld.INCOME -> stringResource(R.string.add_money_to) - TrnTypeOld.EXPENSE -> stringResource(R.string.pay_with) - TrnTypeOld.TRANSFER -> stringResource(R.string.from) - } - - SheetHeader( - percentExpanded = percentExpanded, - label = label, - type = type, - accounts = accounts, - selectedAccount = selectedAccount, - toAccount = toAccount, - onSelectedAccountChanged = onSelectedAccountChanged, - onToAccountChanged = onToAccountChanged, - onAddNewAccount = onAddNewAccount - ) - - val spacerAboveAmount = lerp(40, 16, percentCollapsed) - Spacer(Modifier.height(spacerAboveAmount.dp)) - - if (type == TrnTypeOld.TRANSFER && percentExpanded < 1f) { - TransferRowMini( - percentCollapsed = percentCollapsed, - fromAccount = selectedAccount, - toAccount = toAccount, - onSetExpanded = { - internalExpanded = true - } - ) - } - - Amount( - type = type, - amount = amount, - currency = currency, - label = label, - account = selectedAccount, - showConvertedAmountText = showConvertedAmountText, - percentExpanded = percentExpanded, - onShowAmountModal = { - setAmountModalShown(true) - }, - onAccountMiniClick = { - hideKeyboard(rootView) - internalExpanded = true - }, - ) - - val lastSpacer = lerp(20f, 8f, percentCollapsed) - if (lastSpacer > 0) { - Spacer(Modifier.height(lastSpacer.dp)) - } -// - //system stuff + keyboard padding - Spacer(Modifier.height(densityScope { bottomBarHeight.toDp() })) - Spacer(Modifier.height(keyboardShownInsetDp)) - } - - BottomBar( - keyboardShown = keyboardShown, - expanded = expanded, - internalExpanded = internalExpanded, - setInternalExpanded = { - internalExpanded = it - }, - setBottomBarHeight = { - bottomBarHeight = it - }, - - keyboardShownInsetDp = keyboardShownInsetDp, - navBarPadding = navBarPadding, - - ActionButton = ActionButton - ) - - val amountModalId = remember(initialTransactionId, amount) { - UUID.randomUUID() - } - AmountModal( - id = amountModalId, - visible = amountModalShown, - currency = currency, - initialAmount = amount.takeIf { it > 0 }, - Header = { - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.account), - style = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(16.dp)) - - AccountsRow( - accounts = accounts, - selectedAccount = selectedAccount, - onSelectedAccountChanged = onSelectedAccountChanged, - onAddNewAccount = onAddNewAccount, - childrenTestTag = "amount_modal_account" - ) - }, - amountSpacerTop = 48.dp, - dismiss = { - setAmountModalShown(false) - } - ) { - onAmountChanged(it) - } -} - -@Composable -private fun BottomBar( - keyboardShown: Boolean, - keyboardShownInsetDp: Dp, - setBottomBarHeight: (Int) -> Unit, - expanded: Boolean, - internalExpanded: Boolean, - setInternalExpanded: (Boolean) -> Unit, - navBarPadding: Dp, - ActionButton: @Composable () -> Unit -) { -// val ivyContext = com.ivy.core.ui.temp.ivyWalletCtx() - val screenHeight = LocalConfiguration.current.screenHeightDp.dp - - ActionsRow( - modifier = Modifier - .onSizeChanged { - setBottomBarHeight(it.height) - } - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - val systemOffsetBottom = keyboardShownInsetDp.toPx() - val visibleHeight = placeable.height * 1f - val y = screenHeight.toPx() - visibleHeight - systemOffsetBottom - - layout(placeable.width, placeable.height) { - placeable.place( - 0, - y.roundToInt() - ) - } - } -// .gradientCutBackground() - .padding(bottom = 12.dp) - .padding(bottom = navBarPadding), - lineColor = UI.colors.medium - ) { - Spacer(Modifier.width(24.dp)) - - val expandRotation by animateFloatAsState( - targetValue = if (expanded) 0f else -180f, - animationSpec = springBounce() - ) - - val rootView = LocalView.current - CircleButton( - modifier = Modifier.rotate(expandRotation), - icon = R.drawable.ic_expand_more, - ) { - setInternalExpanded(!internalExpanded || keyboardShown) - hideKeyboard(rootView) - } - - Spacer(Modifier.weight(1f)) - - ActionButton() - - Spacer(Modifier.width(24.dp)) - } -} - -@Composable -private fun TransferRowMini( - percentCollapsed: Float, - fromAccount: AccountOld?, - toAccount: AccountOld?, - onSetExpanded: () -> Unit -) { - Row( - modifier = Modifier - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - val height = placeable.height * (percentCollapsed) - - layout(placeable.width, height.roundToInt()) { - placeable.placeRelative( - x = 0, - y = 0 - ) - } - } - .alpha(percentCollapsed) - .clickableNoIndication { - onSetExpanded() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - val fromColor = fromAccount?.color?.toComposeColor() ?: Ivy - val fromContrastColor = findContrastTextColor(fromColor) - IvyButton( - text = fromAccount?.name ?: "Null", - iconStart = R.drawable.ic_accounts, - backgroundGradient = Gradient.solid(fromColor), - iconTint = fromContrastColor, - textStyle = UI.typo.b2.style( - color = fromContrastColor, - fontWeight = FontWeight.ExtraBold - ), - padding = 10.dp, - ) { - onSetExpanded() - } - - IvyIcon( - icon = R.drawable.ic_arrow_right, - tint = UI.colorsInverted.pure - ) - - val toColor = toAccount?.color?.toComposeColor() ?: Ivy - val toContrastColor = findContrastTextColor(toColor) - IvyButton( - text = toAccount?.name ?: "Null", - iconStart = R.drawable.ic_accounts, - backgroundGradient = Gradient.solid(toColor), - iconTint = toContrastColor, - textStyle = UI.typo.b2.style( - color = toContrastColor, - fontWeight = FontWeight.ExtraBold - ), - padding = 10.dp, - ) { - onSetExpanded() - } - } - - val transferMiniBottomSpacer = 20 * percentCollapsed - if (transferMiniBottomSpacer > 0f) { - Spacer(modifier = Modifier.height(transferMiniBottomSpacer.dp)) - } -} - -@Composable -private fun SheetHeader( - percentExpanded: Float, - label: String, - type: TrnTypeOld, - accounts: List, - selectedAccount: AccountOld?, - toAccount: AccountOld?, - onSelectedAccountChanged: (AccountOld) -> Unit, - onToAccountChanged: (AccountOld) -> Unit, - onAddNewAccount: () -> Unit, -) { - if (percentExpanded > 0.01f) { - Column( - modifier = Modifier - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - -// val x = lerp(0, ivyContext.screenWidth, (1f - percentExpanded)) - val height = placeable.height * percentExpanded - - layout(placeable.width, height.roundToInt()) { - placeable.placeRelative( - x = 0, - y = -(height * (1f - percentExpanded)).roundToInt(), - ) - } - } - .alpha(percentExpanded) - ) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = label, - style = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(if (type == TrnTypeOld.TRANSFER) 8.dp else 16.dp)) - - AccountsRow( - accounts = accounts, - selectedAccount = selectedAccount, - onSelectedAccountChanged = onSelectedAccountChanged, - onAddNewAccount = onAddNewAccount, - childrenTestTag = "from_account" - ) - - if (type == TrnTypeOld.TRANSFER) { - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(start = 32.dp), - text = stringResource(R.string.to), - style = UI.typo.b1.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(8.dp)) - - AccountsRow( - accounts = accounts, - selectedAccount = toAccount, - onSelectedAccountChanged = onToAccountChanged, - onAddNewAccount = onAddNewAccount, - childrenTestTag = "to_account", - ) - } - } - } -} - -@Composable -private fun AccountsRow( - modifier: Modifier = Modifier, - accounts: List, - selectedAccount: AccountOld?, - childrenTestTag: String? = null, - onSelectedAccountChanged: (AccountOld) -> Unit, - onAddNewAccount: () -> Unit -) { - val lazyState = rememberLazyListState() - - LaunchedEffect(accounts, selectedAccount) { - if (selectedAccount != null) { - val selectedIndex = accounts.indexOf(selectedAccount) - if (selectedIndex != -1) { - launch { -// if (TestingContext.inTest) return@launch //breaks UI tests - - lazyState.scrollToItem( - index = selectedIndex, //+1 because Spacer width 24.dp - ) - } - } - } - } - - LazyRow( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - state = lazyState - ) { - item { - Spacer(Modifier.width(24.dp)) - } - - itemsIndexed(accounts) { _, account -> - Account( - account = account, - selected = selectedAccount == account, - testTag = childrenTestTag ?: "account" - ) { - onSelectedAccountChanged(account) - } - } - - item { - AddAccount { - onAddNewAccount() - } - } - - item { - Spacer(Modifier.width(24.dp)) - } - } -} - -@Composable -private fun Account( - account: AccountOld, - selected: Boolean, - testTag: String, - onClick: () -> Unit -) { - val accountColor = account.color.toComposeColor() - val textColor = - if (selected) findContrastTextColor(accountColor) else UI.colorsInverted.pure - - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .thenIf(!selected) { - border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - } - .thenIf(selected) { - background(accountColor, UI.shapes.fullyRounded) - } - .clickable(onClick = onClick) - .testTag(testTag), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - ItemIconSDefaultIcon( - iconName = account.icon, - defaultIcon = R.drawable.ic_custom_account_s, - tint = textColor - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier.padding(vertical = 10.dp), - text = account.name, - style = UI.typo.b2.style( - color = textColor, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.width(8.dp)) -} - -@Composable -private fun AddAccount( - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clip(UI.shapes.fullyRounded) - .border(2.dp, UI.colors.medium, UI.shapes.fullyRounded) - .clickable(onClick = onClick), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(12.dp)) - - IvyIcon( - icon = R.drawable.ic_plus, - tint = UI.colorsInverted.pure - ) - - Spacer(Modifier.width(4.dp)) - - Text( - modifier = Modifier.padding(vertical = 10.dp), - text = stringResource(R.string.add_account), - style = UI.typo.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.width(24.dp)) - } - - Spacer(Modifier.width(8.dp)) -} - -@Composable -private fun Amount( - type: TrnTypeOld, - amount: Double, - currency: String, - percentExpanded: Float, - label: String, - account: AccountOld?, - showConvertedAmountText: String? = null, - onShowAmountModal: () -> Unit, - onAccountMiniClick: () -> Unit, -) { - Row( - modifier = Modifier, - verticalAlignment = Alignment.CenterVertically - ) { - val percentCollapsed = 1f - percentExpanded - val integerFontSize = lerp(40, 30, percentCollapsed) - val spacerInteger = lerp(4, 0, percentCollapsed) - val currencyPaddingTop = lerp(8, 4, percentCollapsed) - val currencyFontSize = lerp(30, 18, percentCollapsed) - - Spacer(Modifier.width(32.dp)) - - if (percentExpanded > 0.01f) { - Spacer( - Modifier.weight( - (1f * percentExpanded).coerceAtLeast(0.01f) - ) - ) - } - - Column() { - BalanceRow( - modifier = Modifier - .clickableNoIndication { - onShowAmountModal() - } - .testTag("edit_amount_balance_row"), - currency = currency, - balance = amount, - - decimalPaddingTop = currencyPaddingTop.dp, - spacerDecimal = spacerInteger.dp, - spacerCurrency = 8.dp, - - - integerFontSize = integerFontSize.sp, - decimalFontSize = 18.sp, - currencyFontSize = currencyFontSize.sp, - - currencyUpfront = false - ) - if (showConvertedAmountText != null) { - Text( - text = showConvertedAmountText, - style = UI.typoSecond.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.SemiBold - ) - ) - } - } - - Spacer(Modifier.weight(1f)) - - if (percentExpanded < 1f && type != TrnTypeOld.TRANSFER) { - LabelAccountMini( - percentExpanded = percentExpanded, - label = label, - account = account, - onClick = onAccountMiniClick - ) - } - - Spacer(Modifier.width(32.dp)) - } -} - -@Composable -private fun LabelAccountMini( - percentExpanded: Float, - label: String, - account: AccountOld?, - onClick: () -> Unit -) { - Column( - modifier = Modifier - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - val width = placeable.width * (1f - percentExpanded) - - layout(width.roundToInt(), placeable.height) { - placeable.placeRelative( - x = 0, - y = 0 - ) - } - } - .alpha(1f - percentExpanded) - .clickableNoIndication( - onClick = onClick - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = label, - style = UI.typoSecond.c.style( - color = UI.colorsInverted.medium, - fontWeight = FontWeight.Medium - ) - ) - - Spacer(Modifier.height(2.dp)) - - Text( - text = account?.name?.toUpperCase(Locale.getDefault()) ?: "", - style = UI.typoSecond.b2.style( - color = UI.colorsInverted.pure, - fontWeight = FontWeight.ExtraBold - ) - ) - } -} - -@Preview -@Composable -private fun Preview() { - IvyPreview { - val acc1 = AccountOld("Cash", color = Green.toArgb()) - - BoxWithConstraints( - modifier = Modifier - .fillMaxSize() - ) { - EditBottomSheet( - amountModalShown = false, - setAmountModalShown = {}, - initialTransactionId = null, - type = TrnTypeOld.INCOME, - ActionButton = { - ModalSet() { - - } - }, - accounts = listOf( - acc1, - AccountOld("DSK", color = GreenDark.toArgb()), - AccountOld("phyre", color = GreenLight.toArgb()), - AccountOld("Revolut", color = IvyDark.toArgb()), - ), - selectedAccount = acc1, - toAccount = null, - amount = 12350.0, - currency = "BGN", - onAmountChanged = {}, - onSelectedAccountChanged = {}, - onToAccountChanged = {}, - onAddNewAccount = {} - ) - } - } -} - -@Preview -@Composable -private fun Preview_Transfer() { - IvyPreview { - val acc1 = AccountOld("Cash", color = Green.toArgb()) - val acc2 = AccountOld("DSK", color = GreenDark.toArgb()) - - BoxWithConstraints( - modifier = Modifier - .fillMaxSize() - ) { - EditBottomSheet( - amountModalShown = false, - setAmountModalShown = {}, - initialTransactionId = UUID.randomUUID(), - ActionButton = { - ModalSave { - - } - }, - type = TrnTypeOld.TRANSFER, - accounts = listOf( - acc1, - acc2, - AccountOld("phyre", color = GreenLight.toArgb(), icon = "cash"), - AccountOld("Revolut", color = IvyDark.toArgb()), - ), - selectedAccount = acc1, - toAccount = acc2, - amount = 12350.0, - currency = "BGN", - onAmountChanged = {}, - onSelectedAccountChanged = {}, - onToAccountChanged = {}, - onAddNewAccount = {} - ) - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/trnedit/Title.kt b/ui-components-old/src/main/java/com/ivy/old/trnedit/Title.kt deleted file mode 100644 index 930d778730..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/trnedit/Title.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.ivy.wallet.ui.edit.core - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.common.Constants -import com.ivy.data.transaction.TrnTypeOld -import com.ivy.design.l0_system.UI -import com.ivy.design.l0_system.style -import com.ivy.design.util.ComponentPreview -import com.ivy.wallet.ui.theme.components.IvyTitleTextField -import com.ivy.wallet.utils.keyboardVisibleState -import com.ivy.wallet.utils.selectEndTextFieldValue -import kotlinx.coroutines.launch -import java.util.* - - -@Composable -fun ColumnScope.Title( - type: TrnTypeOld, - titleFocus: FocusRequester, - initialTransactionId: UUID?, - - titleTextFieldValue: TextFieldValue, - setTitleTextFieldValue: (TextFieldValue) -> Unit, - suggestions: Set, - scrollState: ScrollState? = null, - - onTitleChanged: (String?) -> Unit, - onNext: () -> Unit, -) { - IvyTitleTextField( - modifier = Modifier - .padding(horizontal = 32.dp) - .focusRequester(titleFocus), - dividerModifier = Modifier - .padding(horizontal = 24.dp), - value = titleTextFieldValue, - hint = when (type) { - TrnTypeOld.INCOME -> stringResource(R.string.income_title) - TrnTypeOld.EXPENSE -> stringResource(R.string.expense_title) - TrnTypeOld.TRANSFER -> stringResource(R.string.transfer_title) - }, - keyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next, - capitalization = KeyboardCapitalization.Sentences - ), - keyboardActions = KeyboardActions( - onNext = { - onNext() - } - ) - ) { - setTitleTextFieldValue(it) - onTitleChanged(it.text) - } - - val coroutineScope = rememberCoroutineScope() - Suggestions( - suggestions = suggestions, - ) { suggestion -> - setTitleTextFieldValue(selectEndTextFieldValue(suggestion)) - onTitleChanged(suggestion) - - coroutineScope.launch { - //scroll to top for better UX - scrollState?.animateScrollTo(0) - } - } -} - -@Composable -private fun Suggestions( - suggestions: Set, - onClick: (String) -> Unit -) { - val keyboardVisible by keyboardVisibleState() - if (keyboardVisible) { - if (suggestions.isNotEmpty()) { - for (suggestion in suggestions.take(Constants.SUGGESTIONS_LIMIT)) { - Suggestion(suggestion = suggestion) { - onClick(suggestion) - } - } - } - } -} - -@Composable -private fun Suggestion( - suggestion: String, - onClick: () -> Unit -) { - Text( - modifier = Modifier - .fillMaxWidth() - .clickable { - onClick() - } - .padding(horizontal = 24.dp) - .padding(vertical = 12.dp), - text = suggestion, - style = UI.typo.b2.style( - fontWeight = FontWeight.Medium - ) - ) -} - -@Preview -@Composable -private fun PreviewTitleWithSuggestions() { - ComponentPreview { - Column { - Title( - type = TrnTypeOld.EXPENSE, - titleFocus = FocusRequester(), - initialTransactionId = null, - titleTextFieldValue = selectEndTextFieldValue(""), - setTitleTextFieldValue = {}, - suggestions = setOf( - "Tabu", - "Harem", - "Club 35" - ), - onTitleChanged = {} - ) { - - } - } - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/trnedit/Toolbar.kt b/ui-components-old/src/main/java/com/ivy/old/trnedit/Toolbar.kt deleted file mode 100644 index ba1d65b09f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/trnedit/Toolbar.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.ivy.wallet.ui.edit.core - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ivy.base.R -import com.ivy.data.transaction.TrnTypeOld - -import com.ivy.wallet.ui.theme.components.CloseButton -import com.ivy.wallet.ui.theme.components.DeleteButton -import com.ivy.wallet.ui.theme.components.IvyOutlinedButton -import java.util.* - -@Composable -fun Toolbar( - type: TrnTypeOld, - initialTransactionId: UUID?, - - onDeleteTrnModal: () -> Unit, - onChangeTransactionTypeModal: () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - - CloseButton { - - } - - Spacer(Modifier.weight(1f)) - - when (type) { - TrnTypeOld.INCOME -> { - IvyOutlinedButton( - text = stringResource(R.string.income), - iconStart = R.drawable.ic_income - ) { - onChangeTransactionTypeModal() - } - - Spacer(Modifier.width(12.dp)) - } - TrnTypeOld.EXPENSE -> { - IvyOutlinedButton( - text = stringResource(R.string.expense), - iconStart = R.drawable.ic_expense - ) { - onChangeTransactionTypeModal() - } - - Spacer(Modifier.width(12.dp)) - } - else -> { - //show nothing - } - } - - if (initialTransactionId != null) { - - DeleteButton( - hasShadow = false - ) { - onDeleteTrnModal() - } - - Spacer(Modifier.width(24.dp)) - } - } -} diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/ComposeExt.kt b/ui-components-old/src/main/java/com/ivy/old/utils/ComposeExt.kt deleted file mode 100644 index 807f6b2c49..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/ComposeExt.kt +++ /dev/null @@ -1,189 +0,0 @@ -package com.ivy.wallet.utils - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Paint -import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.UriHandler -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.core.graphics.Insets -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import com.ivy.wallet.ui.theme.Gradient - -fun Modifier.horizontalGradientBackground( - gradient: Gradient -) = drawWithCache { - // Use drawWithCache modifier to create and cache the gradient once size is known or changes. - onDrawBehind { - drawRect( - brush = Brush.horizontalGradient( - startX = 0.0f, - endX = size.width, - colors = listOf(gradient.startColor, gradient.endColor) - ) - ) - } -} - -@Composable -fun windowInsets(): WindowInsetsCompat { - val rootView = LocalView.current - return WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets, rootView) -} - -@Composable -fun systemWindowInsets(): Insets { - val windowInsets = windowInsets() - return windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.navigationBars()) -} - -@Composable -fun statusBarInset(): Int { - val windowInsets = windowInsets() - return windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top -} - -@Composable -fun navigationBarInset(): Int { - return navigationBarInsets().bottom -} - -@Composable -fun navigationBarInsets(): Insets { - val windowInsets = windowInsets() - return windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()) -} - - -@Composable -fun keyboardNavigationWindowInsets(): Insets { - val windowInsets = windowInsets() - return windowInsets.getInsets( - WindowInsetsCompat.Type.ime() - or WindowInsetsCompat.Type.systemBars() - ) -} - -@Composable -fun keyboardOnlyWindowInsets(): Insets { - val windowInsets = windowInsets() - return windowInsets.getInsets( - WindowInsetsCompat.Type.ime() - ) -} - - -@Composable -fun densityScope(densityScope: @Composable Density.() -> T): T { - return with(LocalDensity.current) { densityScope() } -} - -fun MutableState.triggerUpdate() { - try { - this.value = value - } catch (e: Exception) { - } -} - -@Composable -fun LiveData.observeAsNeverEqualState(initial: R): State { - val lifecycleOwner = LocalLifecycleOwner.current - val state = remember { mutableStateOf(initial, policy = neverEqualPolicy()) } - - DisposableEffect(this, lifecycleOwner) { - val observer = Observer { state.value = it } - observe(lifecycleOwner, observer) - onDispose { removeObserver(observer) } - } - return state -} - -fun Modifier.thenIf(condition: Boolean, thanModifier: @Composable Modifier.() -> Modifier) - : Modifier = composed { - if (condition) { - this.thanModifier() - } else this -} - - -fun Modifier.consumeClicks() = clickableNoIndication { - //consume click -} - -fun Modifier.clickableNoIndication( - onClick: () -> Unit -): Modifier = composed { - this.clickable( - interactionSource = remember { MutableInteractionSource() }, - onClick = onClick, - role = null, - indication = null - ) -} - -@Deprecated("use :design-system equivalent") -fun Modifier.drawColoredShadow( - color: Color, - alpha: Float = 0.15f, - borderRadius: Dp = 0.dp, - shadowRadius: Dp = 16.dp, - offsetX: Dp = 0.dp, - offsetY: Dp = 8.dp -) = this.drawBehind { - val transparentColor = android.graphics.Color.toArgb(color.copy(alpha = 0.0f).value.toLong()) - val shadowColor = android.graphics.Color.toArgb(color.copy(alpha = alpha).value.toLong()) - this.drawIntoCanvas { - val paint = Paint() - val frameworkPaint = paint.asFrameworkPaint() - frameworkPaint.color = transparentColor - frameworkPaint.setShadowLayer( - shadowRadius.toPx(), - offsetX.toPx(), - offsetY.toPx(), - shadowColor - ) - it.drawRoundRect( - 0f, - 0f, - this.size.width, - this.size.height, - borderRadius.toPx(), - borderRadius.toPx(), - paint - ) - } -} - -fun selectEndTextFieldValue(text: String?) = TextFieldValue( - text = text ?: "", - selection = TextRange(text?.length ?: 0) -) - -@Composable -fun Dp.toDensityPx() = densityScope { toPx() } - -@Composable -fun Int.toDensityDp() = densityScope { toDp() } - -@Composable -fun Float.toDensityDp() = densityScope { toDp() } - -fun openUrl(uriHandler: UriHandler, url: String) { - uriHandler.openUri(url) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/DateExt.kt b/ui-components-old/src/main/java/com/ivy/old/utils/DateExt.kt deleted file mode 100644 index 09a828d2f0..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/DateExt.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.ivy.wallet.utils - - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import com.ivy.base.R - -import java.time.* -import java.time.format.DateTimeFormatter -import java.util.* -import java.util.concurrent.TimeUnit - - -fun timeNowLocal() = LocalDateTime.now() - - -fun timeNowUTC(): LocalDateTime = LocalDateTime.now(ZoneOffset.UTC) - - -fun dateNowUTC(): LocalDate = LocalDate.now(ZoneOffset.UTC) - -fun startOfDayNowUTC() = dateNowUTC().atStartOfDay() - -fun endOfDayNowUTC() = dateNowUTC().atEndOfDay() - -fun Long.epochSecondToDateTime(): LocalDateTime = - LocalDateTime.ofEpochSecond(this, 0, ZoneOffset.UTC) - -fun LocalDateTime.toEpochSeconds() = this.toEpochSecond(ZoneOffset.UTC) - -fun Long.epochMilliToDateTime(): LocalDateTime = - Instant.ofEpochMilli(this).atZone(ZoneOffset.UTC).toLocalDateTime() - -fun LocalDateTime.toEpochMilli(): Long = millis() - -fun LocalDateTime.millis() = this.toInstant(ZoneOffset.UTC).toEpochMilli() - -fun LocalDateTime.formatNicely( - noWeekDay: Boolean = false, - zone: ZoneId = ZoneOffset.systemDefault() -): String { - val today = dateNowUTC() - val isThisYear = today.year == this.year - - val patternNoWeekDay = "dd MMM" - - if (noWeekDay) { - return if (isThisYear) { - this.formatLocal(patternNoWeekDay) - } else { - this.formatLocal("dd MMM, yyyy") - } - } - - return when (this.toLocalDate()) { - today -> { - com.ivy.core.ui.temp.stringRes( - R.string.today_date, - this.formatLocal(patternNoWeekDay, zone) - ) - } - today.minusDays(1) -> { - com.ivy.core.ui.temp.stringRes( - R.string.yesterday_date, - this.formatLocal(patternNoWeekDay, zone) - ) - } - today.plusDays(1) -> { - com.ivy.core.ui.temp.stringRes( - R.string.tomorrow_date, - this.formatLocal(patternNoWeekDay, zone) - ) - } - else -> { - if (isThisYear) { - this.formatLocal("EEE, dd MMM", zone) - } else { - this.formatLocal("dd MMM, yyyy", zone) - } - } - } -} - -fun LocalDateTime.formatNicelyWithTime( - noWeekDay: Boolean = true, - zone: ZoneId = ZoneOffset.systemDefault() -): String { - val today = dateNowUTC() - val isThisYear = today.year == this.year - - val patternNoWeekDay = "dd MMM HH:mm" - - if (noWeekDay) { - return if (isThisYear) { - this.formatLocal(patternNoWeekDay) - } else { - this.formatLocal("dd MMM, yyyy HH:mm") - } - } - - return when (this.toLocalDate()) { - today -> { - com.ivy.core.ui.temp.stringRes( - R.string.today_date, - this.formatLocal(patternNoWeekDay, zone) - ) - } - today.minusDays(1) -> { - com.ivy.core.ui.temp.stringRes( - R.string.yesterday_date, - this.formatLocal(patternNoWeekDay, zone) - ) - } - today.plusDays(1) -> { - com.ivy.core.ui.temp.stringRes( - R.string.tomorrow, - this.formatLocal(patternNoWeekDay, zone) - ) - } - else -> { - if (isThisYear) { - this.formatLocal("EEE, dd MMM HH:mm", zone) - } else { - this.formatLocal("dd MMM, yyyy HH:mm", zone) - } - } - } -} - -@Composable -fun LocalDateTime.formatLocalTime(): String { - val timeFormat = android.text.format.DateFormat.getTimeFormat(LocalContext.current) - return timeFormat.format(this.millis()) -} - -fun LocalDate.formatDateOnly(): String = this.formatLocal("MMM. dd", ZoneOffset.systemDefault()) - -fun LocalDate.formatDateOnlyWithYear(): String = - this.formatLocal("dd MMM, yyyy", ZoneOffset.systemDefault()) - - -fun LocalDate.formatDateWeekDay(): String = - this.formatLocal("EEE, dd MMM", ZoneOffset.systemDefault()) - -fun LocalDate.formatDateWeekDayLong(): String = - this.formatLocal("EEEE, dd MMM", ZoneOffset.systemDefault()) - -fun LocalDate.formatNicely( - pattern: String = "EEE, dd MMM", - patternNoWeekDay: String = "dd MMM", - zone: ZoneId = ZoneOffset.systemDefault() -): String { - val closeDay = closeDay() - return if (closeDay != null) - "$closeDay, ${this.formatLocal(patternNoWeekDay, zone)}" else this.formatLocal( - pattern, - zone - ) -} - -fun LocalDate.closeDay(): String? { - val today = dateNowUTC() - return when (this) { - today -> { - com.ivy.core.ui.temp.stringRes(R.string.today) - } - today.minusDays(1) -> { - com.ivy.core.ui.temp.stringRes(R.string.yesterday) - } - today.plusDays(1) -> { - com.ivy.core.ui.temp.stringRes(R.string.tomorrow) - } - else -> { - null - } - } -} - -fun LocalDateTime.formatLocal( - pattern: String = "dd MMM yyyy, HH:mm", - zone: ZoneId = ZoneOffset.systemDefault() -): String { - val localDateTime = this.convertUTCtoLocal(zone) - return localDateTime.atZone(zone).format( - DateTimeFormatter - .ofPattern(pattern) - .withLocale(Locale.getDefault()) - .withZone(zone) //this is if you want to display the Zone in the pattern - ) -} - -fun LocalDateTime.format( - pattern: String -): String { - return this.format( - DateTimeFormatter.ofPattern(pattern) - ) -} - -fun LocalDateTime.convertUTCtoLocal(zone: ZoneId = ZoneOffset.systemDefault()): LocalDateTime { - return this.convertUTCto(zone) -} - -fun LocalDateTime.convertUTCto(zone: ZoneId): LocalDateTime { - return plusSeconds(atZone(zone).offset.totalSeconds.toLong()) -} - -fun LocalTime.convertLocalToUTC(): LocalTime { - val offset = timeNowLocal().atZone(ZoneOffset.systemDefault()).offset.totalSeconds.toLong() - return this.minusSeconds(offset) -} - -fun LocalTime.convertUTCToLocal(): LocalTime { - val offset = timeNowLocal().atZone(ZoneOffset.systemDefault()).offset.totalSeconds.toLong() - return this.plusSeconds(offset) -} - -fun LocalDateTime.convertLocalToUTC(): LocalDateTime { - val offset = timeNowLocal().atZone(ZoneOffset.systemDefault()).offset.totalSeconds.toLong() - return this.minusSeconds(offset) -} - -// The timepicker returns time in UTC, but the date picker returns date in LocalTimeZone -// hence use this method to get both date & time in UTC -fun getTrueDate(date: LocalDate, time: LocalTime, convert: Boolean = true): LocalDateTime { - val timeLocal = if (convert) time.convertUTCToLocal() else time - - return timeNowUTC() - .withYear(date.year) - .withMonth(date.monthValue) - .withDayOfMonth(date.dayOfMonth) - .withHour(timeLocal.hour) - .withMinute(timeLocal.minute) - .withSecond(0) - .withNano(0) - .convertLocalToUTC() -} - - -fun LocalDate.formatLocal( - pattern: String = "dd MMM yyyy", - zone: ZoneId = ZoneOffset.systemDefault() -): String { - return this.format( - DateTimeFormatter - .ofPattern(pattern) - .withLocale(Locale.getDefault()) - .withZone(zone) //this is if you want to display the Zone in the pattern - ) -} - -fun LocalDateTime.timeLeft( - from: LocalDateTime = timeNowUTC(), - daysLabel: String = "d", - hoursLabel: String = "h", - minutesLabel: String = "m", - secondsLabel: String = "s" -): String { - val timeLeftMs = this.millis() - from.millis() - if (timeLeftMs <= 0) return com.ivy.core.ui.temp.stringRes(R.string.expired) - - val days = TimeUnit.MILLISECONDS.toDays(timeLeftMs) - var timeLeftAfterCalculations = timeLeftMs - TimeUnit.DAYS.toMillis(days) - - val hours = TimeUnit.MILLISECONDS.toHours(timeLeftAfterCalculations) - timeLeftAfterCalculations -= TimeUnit.HOURS.toMillis(hours) - - val minutes = TimeUnit.MILLISECONDS.toMinutes(timeLeftAfterCalculations) - timeLeftAfterCalculations -= TimeUnit.MINUTES.toMillis(minutes) - - val seconds = TimeUnit.MILLISECONDS.toSeconds(timeLeftAfterCalculations) - - var result = "" - if (days > 0) { - result += "$days$daysLabel " - } - if (hours > 0) { - result += "$hours$hoursLabel " - } - if (minutes > 0) { - result += "$minutes$minutesLabel " - } -// if (seconds > 0) { -// result += "$seconds$secondsLabel " -// } - - return result.trim() -} - -fun startOfMonth(date: LocalDate): LocalDateTime = - date.withDayOfMonth(1).atStartOfDay().convertLocalToUTC() - -fun endOfMonth(date: LocalDate): LocalDateTime = - date.withDayOfMonth(date.lengthOfMonth()).atEndOfDay().convertLocalToUTC() - -fun LocalDate.atEndOfDay(): LocalDateTime = - this.atTime(23, 59, 59) - -/** - * +1 day so things won't fck up with Long overflow - */ -fun beginningOfIvyTime(): LocalDateTime = LocalDateTime.now().minusYears(10) - -fun toIvyFutureTime(): LocalDateTime = timeNowUTC().plusYears(30) - -fun LocalDate.withDayOfMonthSafe(targetDayOfMonth: Int): LocalDate { - val maxDayOfMonth = this.lengthOfMonth() - return this.withDayOfMonth( - if (targetDayOfMonth > maxDayOfMonth) maxDayOfMonth else targetDayOfMonth - ) -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/Event.kt b/ui-components-old/src/main/java/com/ivy/old/utils/Event.kt deleted file mode 100644 index 5a6ff66961..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/Event.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.ivy.wallet.utils - -import android.annotation.SuppressLint -import androidx.compose.runtime.Composable -import androidx.lifecycle.Observer - -/** - * Used as a wrapper for data that is exposed via a LiveData that represents an event. - */ -open class Event(private val content: T) { - - @Suppress("MemberVisibilityCanBePrivate") - var hasBeenHandled = false - private set // Allow external read but not write - - /** - * Returns the content and prevents its use again. - */ - fun getContentIfNotHandled(): T? { - return if (hasBeenHandled) { - null - } else { - hasBeenHandled = true - content - } - } - - /** - * Returns the content, even if it's already been handled. - */ - fun peekContent(): T = content - - @SuppressLint("ComposableNaming") - @Composable - fun handle(block: @Composable (T) -> Unit) { - if (!hasBeenHandled) { - block(content) - hasBeenHandled = true - } - } -} - -/** - * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has - * already been handled. - * - * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. - */ -class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { - override fun onChanged(event: Event?) { - event?.getContentIfNotHandled()?.let { - onEventUnhandledContent(it) - } - } -} - -class NullableParam(val param: T?) diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/FileUtil.kt b/ui-components-old/src/main/java/com/ivy/old/utils/FileUtil.kt deleted file mode 100644 index 0243acd8d5..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/FileUtil.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.ivy.wallet.utils - -import android.content.ContentResolver -import android.content.Context -import android.net.Uri -import android.os.Environment -import android.provider.OpenableColumns -import java.io.* -import java.nio.charset.Charset - -@Deprecated("useless") -fun saveFile( - context: Context, - directoryType: String, - fileName: String, - content: String -) { - val dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - ?: return - val newFile = File("${dirPath}/$fileName") - newFile.createNewFile() - newFile.writeText(content) -} - -fun writeToFile(context: Context, uri: Uri, content: String) { - try { - val contentResolver = context.contentResolver - - contentResolver.openFileDescriptor(uri, "w")?.use { - FileOutputStream(it.fileDescriptor).use { fOut -> - val writer = fOut.writer(charset = Charsets.UTF_16) - writer.write(content) - writer.close() - } - } - } catch (e: FileNotFoundException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } -} - -fun readFile( - context: Context, - uri: Uri, - charset: Charset -): String? { - return try { - val contentResolver = context.contentResolver - - var fileContent: String? = null - - contentResolver.openFileDescriptor(uri, "r")?.use { - FileInputStream(it.fileDescriptor).use { fileInputStream -> - fileContent = readFileContent( - fileInputStream = fileInputStream, - charset = charset - ) - } - } - - fileContent - } catch (e: FileNotFoundException) { - e.printStackTrace() - null - } catch (e: IOException) { - e.printStackTrace() - null - } -} - -@Throws(IOException::class) -private fun readFileContent( - fileInputStream: FileInputStream, - charset: Charset -): String { - BufferedReader(InputStreamReader(fileInputStream, charset)).use { br -> - val sb = StringBuilder() - var line: String? - while (br.readLine().also { line = it } != null) { - sb.append(line) - sb.append('\n') - } - return sb.toString() - } -} - -fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { - ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) - else -> uri.path?.let(::File)?.name -} - -private fun Context.getContentFileName(uri: Uri): String? = runCatching { - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - cursor.moveToFirst() - return@use cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString) - } -}.getOrNull() \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/InputError.kt b/ui-components-old/src/main/java/com/ivy/old/utils/InputError.kt deleted file mode 100644 index 65973da10f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/InputError.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.ivy.wallet.utils - -open class InputError(msg: String) : Exception(msg) \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/IvyAnimation.kt b/ui-components-old/src/main/java/com/ivy/old/utils/IvyAnimation.kt deleted file mode 100644 index 8ec6656136..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/IvyAnimation.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.ivy.wallet.utils - -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring - -fun springBounce( - stiffness: Float = 500f, -) = spring( - dampingRatio = 0.75f, - stiffness = stiffness, -) - -fun springBounceFast() = springBounce( - stiffness = 2000f -) - -fun springBounceMedium() = spring( - dampingRatio = 0.75f, - stiffness = Spring.StiffnessLow, -) - -fun springBounceSlow() = spring( - dampingRatio = 0.75f, - stiffness = Spring.StiffnessVeryLow, -) - -fun springBounceVerySlow() = spring( - dampingRatio = 0.75f, - stiffness = 20f, -) \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/OpResult.kt b/ui-components-old/src/main/java/com/ivy/old/utils/OpResult.kt deleted file mode 100644 index bf2277815f..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/OpResult.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.ivy.wallet.utils - -sealed class OpResult { - object Loading : OpResult() - data class Success(val data: T) : OpResult() - data class Failure(val exception: Exception) : OpResult() { - fun error() = exception.message ?: exception.cause?.message ?: "unknown" - } - - companion object { - fun success(data: T) = Success(data) - fun loading() = Loading - fun failure(e: Exception) = Failure(e) - fun faliure(errMsg: String) = Failure(Exception(errMsg)) - } -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/UIExt.kt b/ui-components-old/src/main/java/com/ivy/old/utils/UIExt.kt deleted file mode 100644 index 9a1428db7e..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/UIExt.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.ivy.wallet.utils - -import android.animation.ArgbEvaluator -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.renderscript.Allocation -import android.renderscript.Element -import android.renderscript.RenderScript -import android.renderscript.ScriptIntrinsicBlur -import android.util.DisplayMetrics -import android.view.View -import android.view.ViewGroup -import android.view.Window -import android.view.WindowInsetsController -import android.view.inputmethod.InputMethodManager -import androidx.annotation.FloatRange -import androidx.annotation.RequiresApi -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.doOnLayout - -import kotlin.math.roundToInt - - -@Composable -fun keyboardVisibleState(): State { - val rootView = LocalView.current - - val keyboardVisible = remember { - mutableStateOf(false) - } - - return keyboardVisible -} - -fun View.addKeyboardListener(keyboardCallback: (visible: Boolean) -> Unit) { - doOnLayout { - //get init state of keyboard - var keyboardVisible = isKeyboardOpen(this) - - //callback as soon as the layout is set with whether the keyboard is open or not - keyboardCallback(keyboardVisible) - - //whenever the layout resizes/changes, callback with the state of the keyboard. - viewTreeObserver.addOnGlobalLayoutListener { - val keyboardUpdateCheck = isKeyboardOpen(this) - //since the observer is hit quite often, only callback when there is a change. - if (keyboardUpdateCheck != keyboardVisible) { - keyboardCallback(keyboardUpdateCheck) - keyboardVisible = keyboardUpdateCheck - } - } - } -} - - -fun isKeyboardOpen(rootView: View): Boolean { - return try { - WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets, rootView) - .isVisible(WindowInsetsCompat.Type.ime()) - } catch (e: Exception) { - e.printStackTrace() - false - } -} - -fun convertDpToPixel(context: Context, dp: Float): Float { - return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) -} - -fun convertDpToPixel(context: Context, dp: Int): Int { - return convertDpToPixel(context, dp.toFloat()).roundToInt() -} - -@SuppressLint("ComposableNaming") -@Composable -fun setStatusBarDarkTextCompat(darkText: Boolean) { - setStatusBarDarkTextCompat( - view = LocalView.current, - darkText = darkText - ) -} - -fun setStatusBarDarkTextCompat(view: View, darkText: Boolean) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - view.windowInsetsController?.setStatusBarDarkText(darkText) - } else { - val window = (view.context as Activity).window - setStatusBarDarkTextOld(window, darkText) - } -} - -@RequiresApi(Build.VERSION_CODES.R) -fun WindowInsetsController.setStatusBarDarkText(darkText: Boolean) { - setSystemBarsAppearance( - if (darkText) WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS else 0, - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, - ) -} - -@Suppress("DEPRECATION") -fun setStatusBarDarkTextOld(window: Window, darkText: Boolean) { - window.decorView.systemUiVisibility = if (darkText) { - window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR // set status bar dark text - } else { - window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() // reset status bar dark text - } -} - -@RequiresApi(api = Build.VERSION_CODES.M) -fun setSystemBarTheme(pActivity: Activity, pIsDark: Boolean) { - // Fetch the current flags. - val lFlags = pActivity.window.decorView.systemUiVisibility - // Update the SystemUiVisibility dependening on whether we want a Light or Dark theme. - pActivity.window.decorView.systemUiVisibility = - if (pIsDark) lFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() else lFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR -} - - -fun lerp(start: Int, end: Int, @FloatRange(from = 0.0, to = 1.0) fraction: Float): Int { - return ((start + fraction * (end - start)).roundToInt()); -} - -fun lerp(start: Float, end: Float, @FloatRange(from = 0.0, to = 1.0) fraction: Float): Float { - return (start + fraction * (end - start)) -} - -fun lerp(start: Double, end: Double, @FloatRange(from = 0.0, to = 1.0) fraction: Double): Double { - return (start + fraction * (end - start)) -} - -fun colorLerp(start: Color, end: Color, fraction: Float): Color { - return Color(ArgbEvaluator().evaluate(fraction, start.toArgb(), end.toArgb()) as Int) -} - -fun hideKeyboard(view: View) { - try { - val imm: InputMethodManager = - view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.windowToken, 0) - } catch (ignore: Exception) { - } -} - -/* - * Creating a Bitmap of view with ARGB_8888. - * -*/ -fun captureView(view: View): Bitmap? { - return try { - val bitmap: Bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - val backgroundDrawable = view.background - if (backgroundDrawable != null) { - backgroundDrawable.draw(canvas) - } else { - canvas.drawColor(Color.Transparent.toArgb()) - } - view.draw(canvas) - bitmap - } catch (e: Exception) { - e.printStackTrace() - null - } -} - -fun Bitmap.blur( - context: Context, - blurRadius: Float = 7.5f, - bitmapScale: Float = 0.4f -): Bitmap { - val width = (this.width * bitmapScale).roundToInt() - val height = (this.height * bitmapScale).roundToInt() - val inputBitmap: Bitmap = Bitmap.createScaledBitmap(this, width, height, false) - val outputBitmap: Bitmap = Bitmap.createBitmap(inputBitmap) - - val rs = RenderScript.create(context) - val theIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)) - val tmpIn = Allocation.createFromBitmap(rs, inputBitmap) - val tmpOut = Allocation.createFromBitmap(rs, outputBitmap) - theIntrinsic.setRadius(blurRadius) - theIntrinsic.setInput(tmpIn) - theIntrinsic.forEach(tmpOut) - tmpOut.copyTo(outputBitmap) - - return outputBitmap -} - -fun postDelayed(delayMs: Long, run: () -> Unit) { - Handler(Looper.getMainLooper()).postDelayed({ run() }, delayMs) -} - -fun post(run: () -> Unit) { - Handler(Looper.getMainLooper()).post { run() } -} - -fun showKeyboard(context: Context) { - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? - imm!!.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) -} - -fun View.setMargin( - topDp: Int? = null, - bottomDp: Int? = null, - leftDp: Int? = null, - rightDp: Int? = null, -) { - val lp = layoutParams as ViewGroup.MarginLayoutParams - if (topDp != null) { - lp.topMargin = convertDpToPixel(context, topDp) - } - if (bottomDp != null) { - lp.bottomMargin = convertDpToPixel(context, bottomDp) - } - if (leftDp != null) { - lp.leftMargin = convertDpToPixel(context, leftDp) - } - if (rightDp != null) { - lp.rightMargin = convertDpToPixel(context, rightDp) - } - layoutParams = lp - requestLayout() - invalidate() -} \ No newline at end of file diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/ValidationExt.kt b/ui-components-old/src/main/java/com/ivy/old/utils/ValidationExt.kt deleted file mode 100644 index 0249529fad..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/ValidationExt.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.ivy.wallet.utils - -import android.annotation.SuppressLint -import android.util.Patterns -import androidx.compose.runtime.Composable - -fun String?.validate( - basicValidationError: () -> InputError = { InputError("field is null or blank") }, - additionalValidation: ((trimmed: String) -> Unit)? = null -): String = this?.trim()?.apply { - if (isBlank()) throw basicValidationError() - additionalValidation?.invoke(this) -} ?: throw basicValidationError() - -fun String?.isNotNullOrBlank(): Boolean { - return this != null && this.isNotBlank() -} - -@SuppressLint("ComposableNaming") -@Composable -fun String?.ifNotNullOrBlank(block: @Composable (String) -> Unit) { - if (this.isNotNullOrBlank()) { - block(this!!) - } -} - -fun CharSequence?.isValidEmail() = - !isNullOrEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches() diff --git a/ui-components-old/src/main/java/com/ivy/old/utils/WalletUtil.kt b/ui-components-old/src/main/java/com/ivy/old/utils/WalletUtil.kt deleted file mode 100644 index 09b1b77820..0000000000 --- a/ui-components-old/src/main/java/com/ivy/old/utils/WalletUtil.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.ivy.wallet.utils - -fun balancePrefix( - income: Double, - expenses: Double -): String? { - return when { - expenses != 0.0 && income != 0.0 -> { - null - } - expenses < 0.0 && income == 0.0 -> { - "-" - } - income > 0.0 && expenses == 0.0 -> { - "+" - } - else -> null - } -} \ No newline at end of file diff --git a/web-view/.gitignore b/web-view/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/web-view/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/web-view/README.md b/web-view/README.md deleted file mode 100644 index e1d0f1ac1e..0000000000 --- a/web-view/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 🚧 Module under construction... - -If it hardly works, it's filled with bad code and anti-patterns anyway... - -### To see how a proper should look like refer to: - -- **[:core](../core)**: responsible for Ivy Wallet's domain -- **[:home](../home/)**: Ivy wallet's home screen. - -Apologies for the inconvenience! I'm a solo dev + the help of our awesome Ivy contributors. If you -want to support us: - -1. Star our repo. - [![GitHub Repo stars](https://img.shields.io/github/stars/Ivy-Apps/ivy-wallet?style=social)](https://github.com/Ivy-Apps/ivy-wallet/stargazers) -2. Have a look at our [Contributors Guide](../CONTRIBUTING.md). \ No newline at end of file diff --git a/web-view/build.gradle.kts b/web-view/build.gradle.kts deleted file mode 100644 index 4054850417..0000000000 --- a/web-view/build.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ -import com.ivy.buildsrc.Hilt - -apply() - -plugins { - `android-library` - `kotlin-android` -} - -dependencies { - Hilt() - implementation(project(":common:main")) - implementation(project(":design-system")) - implementation(project(":ui-components-old")) - implementation(project(":app-base")) - implementation(project(":core:ui")) - implementation(project(":core:data-model")) - implementation(project(":navigation")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) - implementation(project(":core:exchange-provider")) -} \ No newline at end of file diff --git a/web-view/src/main/AndroidManifest.xml b/web-view/src/main/AndroidManifest.xml deleted file mode 100644 index ac9526c207..0000000000 --- a/web-view/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/web-view/src/main/java/com/ivy/web/WebViewScreen.kt b/web-view/src/main/java/com/ivy/web/WebViewScreen.kt deleted file mode 100644 index 887a8fdd69..0000000000 --- a/web-view/src/main/java/com/ivy/web/WebViewScreen.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.ivy.web - -import android.annotation.SuppressLint -import android.webkit.WebChromeClient -import android.webkit.WebView -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.webkit.WebSettingsCompat -import androidx.webkit.WebViewFeature - -import com.ivy.wallet.ui.theme.components.BackButtonType -import com.ivy.wallet.ui.theme.components.IvyToolbar - -@SuppressLint("SetJavaScriptEnabled") -@ExperimentalFoundationApi -@Composable -fun BoxWithConstraintsScope.WebViewScreen() { -// UI(url = screen.url) -} - -@SuppressLint("SetJavaScriptEnabled") -@ExperimentalFoundationApi -@Composable -private fun BoxWithConstraintsScope.UI(url: String) { - Column( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - ) { - - IvyToolbar( - onBack = { -// nav.onBackPressed() - }, - backButtonType = BackButtonType.CLOSE, - paddingTop = 8.dp, - paddingBottom = 8.dp - ) - - //Android WebView should not be a in a scroll container :/ - //because anchor links doesn't work - //https://stackoverflow.com/questions/3039555/android-webview-anchor-link-jump-link-not-working - AndroidView( - factory = ::WebView, - update = { webView -> - //Activate Dark mode if the user uses Dark theme & it's supported - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { - val forceDarkMode = if (false) - WebSettingsCompat.FORCE_DARK_ON else WebSettingsCompat.FORCE_DARK_OFF - WebSettingsCompat.setForceDark( - webView.settings, - forceDarkMode - ) - } - - //Chrome Client is compatible with most of websites - webView.webChromeClient = WebChromeClient() - webView.settings.javaScriptEnabled = true - webView.loadUrl(url) - } - ) - } -} - - diff --git a/widgets/build.gradle.kts b/widgets/build.gradle.kts index f21f83898c..2ca08c8852 100644 --- a/widgets/build.gradle.kts +++ b/widgets/build.gradle.kts @@ -1,4 +1,5 @@ import com.ivy.buildsrc.DataStore +import com.ivy.buildsrc.Glance import com.ivy.buildsrc.Hilt apply() @@ -19,10 +20,8 @@ dependencies { implementation(project(":common:main")) implementation(project(":design-system")) implementation(project(":core:data-model")) - implementation(project(":app-base")) implementation(project(":core:ui")) - implementation(project(":temp-domain")) - implementation(project(":temp-persistence")) + Glance() DataStore(api = false) } \ No newline at end of file diff --git a/widgets/src/main/AndroidManifest.xml b/widgets/src/main/AndroidManifest.xml index d0266a7785..68c2f21902 100644 --- a/widgets/src/main/AndroidManifest.xml +++ b/widgets/src/main/AndroidManifest.xml @@ -1,37 +1,36 @@ - + - - - - - - + + + + + + + + - - - - - - + + + + + + + + - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/widgets/src/main/java/com/ivy/widgets/AddTransactionWidget.kt b/widgets/src/main/java/com/ivy/widgets/AddTransactionWidget.kt deleted file mode 100644 index d6b542338f..0000000000 --- a/widgets/src/main/java/com/ivy/widgets/AddTransactionWidget.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.ivy.widgets - -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.Context -import android.content.Intent -import android.widget.RemoteViews - -class AddTransactionWidget : AppWidgetProvider() { - - companion object { - fun updateBroadcast(context: Context) { - WidgetBase.updateBroadcast(context, AddTransactionWidget::class.java) - } - } - - override fun onEnabled(context: Context) { - super.onEnabled(context) - updateBroadcast(context) - } - - //--------------------------- ---------------------------------------------------- - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - for (appWidgetId in appWidgetIds) { - updateAppWidget(context, appWidgetManager, appWidgetId) - } - } - - private fun updateAppWidget( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetId: Int - ) { - val rv = RemoteViews(context.packageName, R.layout.widget_add_transaction) - val clickSetup = AddTransactionWidgetClick.Setup(context, rv, appWidgetId) - - clickSetup.clickListener(R.id.ivIncome, AddTransactionWidgetClick.ACTION_ADD_INCOME) - clickSetup.clickListener(R.id.tvIncome, AddTransactionWidgetClick.ACTION_ADD_INCOME) - - clickSetup.clickListener(R.id.ivExpense, AddTransactionWidgetClick.ACTION_ADD_EXPENSE) - clickSetup.clickListener(R.id.tvExpense, AddTransactionWidgetClick.ACTION_ADD_EXPENSE) - - clickSetup.clickListener(R.id.ivTransfer, AddTransactionWidgetClick.ACTION_ADD_TRANSFER) - clickSetup.clickListener(R.id.tvTransfer, AddTransactionWidgetClick.ACTION_ADD_TRANSFER) - - appWidgetManager.updateAppWidget(appWidgetId, rv) - } - - override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) - val widgetClick = AddTransactionWidgetClick() - widgetClick.handleClick(context, intent) - } -} \ No newline at end of file diff --git a/widgets/src/main/java/com/ivy/widgets/AddTransactionWidgetClick.kt b/widgets/src/main/java/com/ivy/widgets/AddTransactionWidgetClick.kt deleted file mode 100644 index 1e970735e4..0000000000 --- a/widgets/src/main/java/com/ivy/widgets/AddTransactionWidgetClick.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.ivy.widgets - -import android.app.PendingIntent -import android.appwidget.AppWidgetManager -import android.content.Context -import android.content.Intent -import android.widget.RemoteViews -import androidx.annotation.IdRes -import com.ivy.data.transaction.TrnTypeOld - -class AddTransactionWidgetClick { - companion object { - const val ACTION_ADD_INCOME = "com.ivy.wallet.ACTION_ADD_INCOME" - const val ACTION_ADD_EXPENSE = "com.ivy.wallet.ACTION_ADD_EXPENSE" - const val ACTION_ADD_TRANSFER = "com.ivy.wallet.ACTION_ADD_TRANSFER" - } - - //============================= ======================================================= - fun handleClick(context: Context, intent: Intent) { - when (intent.action) { - ACTION_ADD_INCOME -> { - context.startActivity( - com.ivy.core.ui.temp.GlobalProvider.rootIntent.addTransactionStart( - context = context, - type = TrnTypeOld.INCOME - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } - ACTION_ADD_EXPENSE -> { - context.startActivity( - com.ivy.core.ui.temp.GlobalProvider.rootIntent.addTransactionStart( - context = context, - type = TrnTypeOld.EXPENSE - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } - ACTION_ADD_TRANSFER -> { - context.startActivity( - com.ivy.core.ui.temp.GlobalProvider.rootIntent.addTransactionStart( - context = context, - type = TrnTypeOld.TRANSFER - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } - else -> return - } - } - - //============================= ======================================================= - //------------------------------ ------------------------------------------------------- - class Setup( - private val context: Context, - private val rv: RemoteViews, - private val appWidgetId: Int - ) { - fun clickListener(@IdRes viewId: Int, action: String) { - val actionIntent = newActionIntent(context, appWidgetId, action) - rv.setOnClickPendingIntent(viewId, actionIntent) - } - - private fun newActionIntent( - context: Context, - appWidgetId: Int, - action: String - ): PendingIntent { - val intent = Intent(context, AddTransactionWidget::class.java) - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - intent.action = action - return PendingIntent.getBroadcast( - context, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - } - } //----------------------------- ------------------------------------------------------- -} \ No newline at end of file diff --git a/widgets/src/main/java/com/ivy/widgets/AddTransactionWidgetCompact.kt b/widgets/src/main/java/com/ivy/widgets/AddTransactionWidgetCompact.kt deleted file mode 100644 index b5ac34ed5a..0000000000 --- a/widgets/src/main/java/com/ivy/widgets/AddTransactionWidgetCompact.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.ivy.widgets - -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.Context -import android.content.Intent -import android.widget.RemoteViews - -class AddTransactionWidgetCompact : AppWidgetProvider() { - - companion object { - fun updateBroadcast(context: Context) { - WidgetBase.updateBroadcast(context, AddTransactionWidgetCompact::class.java) - } - } - - override fun onEnabled(context: Context) { - super.onEnabled(context) - updateBroadcast(context) - } - - //--------------------------- ---------------------------------------------------- - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - for (appWidgetId in appWidgetIds) { - updateAppWidget(context, appWidgetManager, appWidgetId) - } - } - - private fun updateAppWidget( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetId: Int - ) { - val rv = RemoteViews(context.packageName, R.layout.widget_add_transaction_compact) - val clickSetup = AddTransactionWidgetClick.Setup(context, rv, appWidgetId) - - clickSetup.clickListener(R.id.ivIncome, AddTransactionWidgetClick.ACTION_ADD_INCOME) - - clickSetup.clickListener(R.id.ivExpense, AddTransactionWidgetClick.ACTION_ADD_EXPENSE) - - clickSetup.clickListener(R.id.ivTransfer, AddTransactionWidgetClick.ACTION_ADD_TRANSFER) - - appWidgetManager.updateAppWidget(appWidgetId, rv) - } - - override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) - val widgetClick = AddTransactionWidgetClick() - widgetClick.handleClick(context, intent) - } -} \ No newline at end of file diff --git a/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidget.kt b/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidget.kt deleted file mode 100644 index a44c88aa12..0000000000 --- a/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidget.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.ivy.widgets - -import android.appwidget.AppWidgetManager -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import androidx.glance.appwidget.state.updateAppWidgetState -import androidx.glance.currentState -import androidx.glance.state.PreferencesGlanceStateDefinition -import com.ivy.core.ui.temp.trash.IvyWalletCtx -import com.ivy.wallet.io.persistence.SharedPrefs -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class WalletBalanceWidget : GlanceAppWidget() { - - @Composable - override fun Content() { - val prefs = currentState() - val appLocked = prefs[booleanPreferencesKey("appLocked")] ?: false - val balance = prefs[stringPreferencesKey("balance")] ?: "0.00" - val currency = prefs[stringPreferencesKey("currency")] ?: "USD" - val income = prefs[stringPreferencesKey("income")] ?: "0.00" - val expense = prefs[stringPreferencesKey("expense")] ?: "0.00" - - WalletBalanceWidgetContent(appLocked, balance, currency, income, expense) - } - -} - -@AndroidEntryPoint -class WalletBalanceReceiver : GlanceAppWidgetReceiver() { - - override val glanceAppWidget: GlanceAppWidget = WalletBalanceWidget() - private val coroutineScope = MainScope() - -// @Inject -// lateinit var walletBalanceAct: CalcWalletBalanceAct -// -// @Inject -// lateinit var settingsAct: SettingsAct -// -// @Inject -// lateinit var accountsAct: AccountsActOld -// -// @Inject -// lateinit var calcIncomeExpenseAct: CalcIncomeExpenseAct - - @Inject - lateinit var ivyContext: IvyWalletCtx - - @Inject - lateinit var sharedPrefs: SharedPrefs - - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - super.onUpdate(context, appWidgetManager, appWidgetIds) - updateData(context) - } - - private fun updateData(context: Context) { - coroutineScope.launch { -// val settings = settingsAct(Unit) -// val appLocked = ioThread { sharedPrefs.getBoolean(SharedPrefs.APP_LOCK_ENABLED, false) } -// val currency = settings.baseCurrency -// val balance = walletBalanceAct(CalcWalletBalanceAct.Input(baseCurrency = currency)) -// val accounts = accountsAct(Unit) -// val period = ivyContext.selectedPeriod -// val incomeExpense = calcIncomeExpenseAct( -// CalcIncomeExpenseAct.Input( -// baseCurrency = settings.baseCurrency, -// accounts = accounts, -// range = period.toRange(ivyContext.startDayOfMonth).toCloseTimeRange() -// ) -// ) - - val glanceId = - GlanceAppWidgetManager(context).getGlanceIds(WalletBalanceWidget::class.java) - .firstOrNull() - glanceId?.let { - updateAppWidgetState(context, PreferencesGlanceStateDefinition, it) { pref -> - pref.toMutablePreferences().apply { -// this[booleanPreferencesKey("appLocked")] = appLocked -// this[stringPreferencesKey("balance")] = shortenAmount(balance.toDouble()) -// this[stringPreferencesKey("currency")] = currency -// this[stringPreferencesKey("income")] = -// shortenAmount(incomeExpense.income.toDouble()) -// this[stringPreferencesKey("expense")] = -// shortenAmount(incomeExpense.expense.toDouble()) - } - } - glanceAppWidget.update(context, it) - } - } - } -} \ No newline at end of file diff --git a/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidgetActions.kt b/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidgetActions.kt deleted file mode 100644 index e1f7f0849e..0000000000 --- a/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidgetActions.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.ivy.widgets - -import android.content.Context -import android.content.Intent -import androidx.glance.GlanceId -import androidx.glance.action.ActionParameters -import androidx.glance.appwidget.action.ActionCallback -import com.ivy.data.transaction.TrnTypeOld - -class WalletBalanceButtonsAction : ActionCallback { - override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - when (parameters[walletBtnActParam]) { - AddTransactionWidgetClick.ACTION_ADD_INCOME -> { - context.startActivity( - com.ivy.core.ui.temp.GlobalProvider.rootIntent.addTransactionStart( - context = context, - type = TrnTypeOld.INCOME - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } - AddTransactionWidgetClick.ACTION_ADD_EXPENSE -> { - context.startActivity( - com.ivy.core.ui.temp.GlobalProvider.rootIntent.addTransactionStart( - context = context, - type = TrnTypeOld.EXPENSE - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } - AddTransactionWidgetClick.ACTION_ADD_TRANSFER -> { - context.startActivity( - com.ivy.core.ui.temp.GlobalProvider.rootIntent.addTransactionStart( - context = context, - type = TrnTypeOld.TRANSFER - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } - else -> return - } - } -} - -class WalletBalanceWidgetClickAction : ActionCallback { - override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - context.startActivity( - com.ivy.core.ui.temp.GlobalProvider.rootIntent.getIntent( - context = context - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } -} - -val walletBtnActParam = ActionParameters.Key("wallet_balance_button_action") \ No newline at end of file diff --git a/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidgetContent.kt b/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidgetContent.kt deleted file mode 100644 index c0894e8c61..0000000000 --- a/widgets/src/main/java/com/ivy/widgets/WalletBalanceWidgetContent.kt +++ /dev/null @@ -1,213 +0,0 @@ -package com.ivy.widgets - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.glance.GlanceModifier -import androidx.glance.Image -import androidx.glance.ImageProvider -import androidx.glance.action.actionParametersOf -import androidx.glance.action.clickable -import androidx.glance.appwidget.action.actionRunCallback -import androidx.glance.background -import androidx.glance.layout.* -import androidx.glance.text.FontWeight -import androidx.glance.text.Text -import androidx.glance.text.TextAlign -import androidx.glance.text.TextStyle -import androidx.glance.unit.ColorProvider - -@Composable -fun WalletBalanceWidgetContent( - appLocked: Boolean, - balance: String, - currency: String, - income: String, - expense: String -) { - Box( - GlanceModifier - .background(ImageProvider(R.drawable.shape_widget_background)) - .clickable(actionRunCallback()) - ) { - Column( - modifier = GlanceModifier.fillMaxSize(), - ) { - if (appLocked) { - Text( - modifier = GlanceModifier.fillMaxSize(), - text = "App locked", - style = TextStyle( - fontSize = 30.sp, - color = ColorProvider(Color.White), - textAlign = TextAlign.Center - ) - ) - } else { - BalanceSection(balance, currency) - IncomeExpenseSection(income, expense, currency) - ButtonsSection() - } - } - } -} - -@Composable -fun RowScope.WidgetClickableItem( - @DrawableRes image: Int, - @StringRes text: Int, -) { - Column( - GlanceModifier - .defaultWeight() - .clickable( - actionRunCallback( - parameters = actionParametersOf( - walletBtnActParam to when (text) { - R.string.income -> AddTransactionWidgetClick.ACTION_ADD_INCOME - R.string.expense -> AddTransactionWidgetClick.ACTION_ADD_EXPENSE - R.string.transfer -> AddTransactionWidgetClick.ACTION_ADD_TRANSFER - else -> return - } - ) - ) - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - modifier = GlanceModifier.size(52.dp), - provider = ImageProvider(image), - contentDescription = null - ) -// Spacer(GlanceModifier.height(8.dp)) -// Text( -// text = stringRes(text), -// style = TextStyle( -// fontSize = 12.sp, -// fontWeight = FontWeight.Bold, -// color = ColorProvider(Color.White) -// ) -// ) - } -} - -@Composable -fun BalanceSection( - balance: String, - currency: String -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = GlanceModifier.padding(start = 14.dp, top = 14.dp), - ) { - Text( - text = currency, - style = TextStyle( - fontSize = 30.sp, - color = ColorProvider(Color.White) - ) - ) - Spacer(GlanceModifier.width(10.dp)) - Text( - text = balance, - style = TextStyle( - fontSize = 34.sp, - fontWeight = FontWeight.Bold, - color = ColorProvider(Color.White) - ) - ) - } -} - -@Composable -fun IncomeExpenseSection( - income: String, - expense: String, - currency: String -) { - Row( - GlanceModifier.fillMaxWidth() - .padding(start = 14.dp, end = 14.dp, top = 12.dp, bottom = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Row( - GlanceModifier - .padding(10.dp) - .defaultWeight() - .background(ImageProvider(R.drawable.income_shape_widget_backgroud)), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - ImageProvider(R.drawable.ic_income_white), - com.ivy.core.ui.temp.stringRes(R.string.income) - ) - Spacer(GlanceModifier.width(8.dp)) - Text( - text = income, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = ColorProvider(Color.White) - ) - ) - Spacer(GlanceModifier.width(4.dp)) - Text( - text = currency, - style = TextStyle( - fontSize = 16.sp, - color = ColorProvider(Color.White), - ) - ) - } - Spacer(GlanceModifier.width(8.dp)) - Row( - GlanceModifier - .padding(10.dp) - .defaultWeight() - .background(ImageProvider(R.drawable.expense_shape_widget_backgroun)), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - ImageProvider(R.drawable.ic_expense), - com.ivy.core.ui.temp.stringRes(R.string.expense) - ) - Spacer(GlanceModifier.width(8.dp)) - Text( - text = expense, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = ColorProvider(Color.Black) - ) - ) - Spacer(GlanceModifier.width(4.dp)) - Text( - text = currency, - style = TextStyle( - fontSize = 16.sp, - color = ColorProvider(Color.Black) - ) - ) - } - } -} - -@Composable -fun ButtonsSection() { - val buttons = listOf( - R.drawable.ic_widget_income to R.string.income, - R.drawable.ic_widget_expense to R.string.expense, - R.drawable.ic_widget_transfer to R.string.transfer - ) - Row( - GlanceModifier.fillMaxWidth().padding(10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - buttons.forEach { (image, text) -> - WidgetClickableItem(image = image, text = text) - } - } -} \ No newline at end of file diff --git a/widgets/src/main/java/com/ivy/widgets/WidgetBase.kt b/widgets/src/main/java/com/ivy/widgets/WidgetBase.kt deleted file mode 100644 index aecf4473bc..0000000000 --- a/widgets/src/main/java/com/ivy/widgets/WidgetBase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.ivy.widgets - -import android.appwidget.AppWidgetManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import timber.log.Timber - -class WidgetBase { - companion object { - fun updateBroadcast(context: Context, widget: Class) { - Timber.d("update()") - val intent = Intent(context, widget) - intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE - val appWidgetManager = AppWidgetManager.getInstance(context) - intent.putExtra( - AppWidgetManager.EXTRA_APPWIDGET_IDS, - getAppWidgetIds( - context = context, - appWidgetManager = appWidgetManager, - widget = widget - ) - ) - context.sendBroadcast(intent) - } - - private fun getAppWidgetIds( - context: Context, - appWidgetManager: AppWidgetManager, - widget: Class - ): IntArray? { - val ivyWidgetComponent = - ComponentName(context, widget) - return appWidgetManager.getAppWidgetIds(ivyWidgetComponent) - } - } - -} \ No newline at end of file