From d769df6f6b0b3b76119e4c15bb100581aee6dae1 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Mon, 21 Sep 2026 17:36:32 +0500 Subject: [PATCH] add ability to hide accounts implements #50 --- .../sh/sar/basedbank/ui/home/HomeActivity.kt | 4 +- .../ui/home/SettingsLoginsFragment.kt | 197 +++++++++++++++++- .../sh/sar/basedbank/util/CredentialStore.kt | 9 + 3 files changed, 201 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index b762899..8288145 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -990,10 +990,12 @@ fun applyNavLabelVisibility() { } } - /** Filters accounts whose profileId the user has hidden in settings. */ + /** Filters accounts whose profileId or account number the user has hidden in settings. */ private fun List.filterVisibleAccounts(): List { val store = CredentialStore(this@HomeActivity) + val hiddenAccountNumbers = store.getHiddenAccountNumbers() return filter { acc -> + if (acc.accountNumber in hiddenAccountNumbers) return@filter false when (acc.bank) { "MIB" -> { val loginId = acc.loginTag.removePrefix("mib_") diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt index 14e118b..f4a9969 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt @@ -30,6 +30,7 @@ import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R import sh.sar.basedbank.api.bml.BmlProfile import sh.sar.basedbank.api.mib.MibProfile +import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.TransactionCache import sh.sar.basedbank.databinding.FragmentSettingsLoginsBinding import sh.sar.basedbank.ui.login.LoginActivity @@ -486,6 +487,98 @@ class SettingsLoginsFragment : Fragment() { } } + /** A single account row with a visibility toggle; [indent] nests it under a parent profile row (tree view). */ + private fun addAccountRow( + ctx: Context, + container: LinearLayout, + dp: Float, + acc: BankAccount, + hiddenAccounts: MutableSet, + indent: Boolean + ): Pair { + val row = LinearLayout(ctx).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also { + it.bottomMargin = (4 * dp).toInt() + if (indent) it.marginStart = (28 * dp).toInt() + } + } + val textCol = LinearLayout(ctx).apply { + orientation = LinearLayout.VERTICAL + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + } + val nameAppearance = if (indent) com.google.android.material.R.style.TextAppearance_Material3_BodySmall + else com.google.android.material.R.style.TextAppearance_Material3_BodyMedium + textCol.addView(TextView(ctx).apply { + text = acc.accountBriefName.ifBlank { acc.accountTypeName.ifBlank { acc.accountNumber } } + setTextAppearance(nameAppearance) + }) + val typeLabel = sh.sar.basedbank.util.AccountListParser.from(acc)?.typeLabel + ?: if (acc.bank == "BML") sh.sar.basedbank.util.bmlapi.BmlDashboardParser.productLabel(acc.accountTypeName) + else acc.accountTypeName.trim() + textCol.addView(TextView(ctx).apply { + text = listOfNotNull(acc.accountNumber, typeLabel.takeIf { it.isNotBlank() }, acc.currencyName.takeIf { it.isNotBlank() }) + .joinToString(" · ") + setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall) + alpha = 0.6f + }) + val toggle = MaterialSwitch(ctx).apply { + isChecked = acc.accountNumber !in hiddenAccounts + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply { + marginStart = (4 * dp).toInt() + } + } + row.addView(textCol) + row.addView(toggle) + container.addView(row) + return acc to toggle + } + + /** Nests [accounts] directly under their parent profile row, without a header — the tree's leaves. */ + private fun addNestedAccountRows( + ctx: Context, + container: LinearLayout, + dp: Float, + accounts: List, + hiddenAccounts: MutableSet + ): List> = + accounts.map { addAccountRow(ctx, container, dp, it, hiddenAccounts, indent = true) } + + /** Builds a headered, flat "Accounts" section for accounts with no profile to nest under. */ + private fun addAccountsSection( + ctx: Context, + container: LinearLayout, + dp: Float, + accounts: List, + hiddenAccounts: MutableSet, + showDivider: Boolean + ): List> { + if (accounts.isEmpty()) return emptyList() + if (showDivider) { + container.addView(View(ctx).apply { + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (1 * dp).toInt()).also { + it.topMargin = (12 * dp).toInt(); it.bottomMargin = (12 * dp).toInt() + } + setBackgroundColor(0x1F000000) + }) + } + container.addView(TextView(ctx).apply { + text = getString(R.string.accounts) + setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelMedium) + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also { + it.bottomMargin = (8 * dp).toInt() + } + }) + return accounts.map { addAccountRow(ctx, container, dp, it, hiddenAccounts, indent = false) } + } + + /** Merges this login's account-hide choices into the global hidden-accounts set, leaving other logins untouched. */ + private fun persistHiddenAccounts(store: CredentialStore, scopedAccounts: List, hiddenAccounts: Set) { + val scopedNumbers = scopedAccounts.map { it.accountNumber }.toSet() + store.setHiddenAccountNumbers((store.getHiddenAccountNumbers() - scopedNumbers) + hiddenAccounts) + } + private fun showMibLoginDetails( store: CredentialStore, loginId: String, @@ -496,6 +589,10 @@ class SettingsLoginsFragment : Fragment() { val dp = ctx.resources.displayMetrics.density val originalHidden = store.getHiddenMibProfileIds(loginId) val hidden = originalHidden.toMutableSet() + val app = requireActivity().application as BasedBankApp + val loginAccounts = app.mibAccounts.filter { it.loginTag == "mib_$loginId" } + val originalHiddenAccounts = loginAccounts.map { it.accountNumber }.filter { it in store.getHiddenAccountNumbers() }.toSet() + val hiddenAccounts = originalHiddenAccounts.toMutableSet() val scroll = android.widget.ScrollView(ctx) val container = LinearLayout(ctx).apply { @@ -538,7 +635,10 @@ class SettingsLoginsFragment : Fragment() { }) } - // Build toggle rows — wired up after dialog.show() so we can reference the Save button + // Build toggle rows — wired up after dialog.show() so we can reference the Save button. + // Each profile's own accounts nest directly beneath it (tree view), since one profile + // can have multiple accounts. + val accountRows = mutableListOf>() val toggleRows = mibProfiles.map { p -> val row = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL @@ -579,16 +679,24 @@ class SettingsLoginsFragment : Fragment() { row.addView(pencil) row.addView(toggle) container.addView(row) + accountRows += addNestedAccountRows(ctx, container, dp, loginAccounts.filter { it.profileId == p.profileId }, hiddenAccounts) p to toggle } + // Accounts that don't belong to any known profile still need to be reachable. + val unassignedAccounts = loginAccounts.filter { acc -> mibProfiles.none { it.profileId == acc.profileId } } + accountRows += addAccountsSection( + ctx, container, dp, unassignedAccounts, hiddenAccounts, + showDivider = mibProfiles.isNotEmpty() || profile != null + ) + fun updateToggleStates(saveBtn: android.widget.Button) { val visibleCount = mibProfiles.count { it.profileId !in hidden } toggleRows.forEach { (p, toggle) -> // Disable the sole remaining visible toggle so it can't be turned off toggle.isEnabled = !(toggle.isChecked && visibleCount == 1) } - saveBtn.isEnabled = hidden != originalHidden && visibleCount >= 1 + saveBtn.isEnabled = (hidden != originalHidden || hiddenAccounts != originalHiddenAccounts) && visibleCount >= 1 } val dialog = MaterialAlertDialogBuilder(ctx) @@ -612,8 +720,16 @@ class SettingsLoginsFragment : Fragment() { } } + accountRows.forEach { (acc, toggle) -> + toggle.setOnCheckedChangeListener { _, checked -> + if (checked) hiddenAccounts.remove(acc.accountNumber) else hiddenAccounts.add(acc.accountNumber) + updateToggleStates(saveBtn) + } + } + saveBtn.setOnClickListener { store.setHiddenMibProfileIds(loginId, hidden) + persistHiddenAccounts(store, loginAccounts, hiddenAccounts) clearAllCaches(ctx) dialog.dismiss() (activity as? HomeActivity)?.relogin() @@ -638,6 +754,10 @@ class SettingsLoginsFragment : Fragment() { if (hidden.add(id)) store.setHiddenBmlProfileIds(loginId, hidden) } val originalHidden = hidden.toSet() + val app = requireActivity().application as BasedBankApp + val loginAccounts = app.bmlAccounts.filter { it.loginTag == "bml_$loginId" } + val originalHiddenAccounts = loginAccounts.map { it.accountNumber }.filter { it in store.getHiddenAccountNumbers() }.toSet() + val hiddenAccounts = originalHiddenAccounts.toMutableSet() val scroll = android.widget.ScrollView(ctx) val container = LinearLayout(ctx).apply { @@ -683,6 +803,9 @@ class SettingsLoginsFragment : Fragment() { }) } + // Each profile's own accounts nest directly beneath it (tree view), since one profile + // can have multiple accounts. + val accountRows = mutableListOf>() val toggleRows = bmlProfiles.map { p -> val avatarIv = makeCircleAvatarView(ctx, 36) val currentBitmap = ProfileImageStore.load(ctx, ProfileImageStore.bmlKey(p.profileId)) @@ -730,15 +853,23 @@ class SettingsLoginsFragment : Fragment() { row.addView(pencil) row.addView(toggle) container.addView(row) + accountRows += addNestedAccountRows(ctx, container, dp, loginAccounts.filter { it.profileId == p.profileId }, hiddenAccounts) p to toggle } + // Accounts that don't belong to any known profile still need to be reachable. + val unassignedAccounts = loginAccounts.filter { acc -> bmlProfiles.none { it.profileId == acc.profileId } } + accountRows += addAccountsSection( + ctx, container, dp, unassignedAccounts, hiddenAccounts, + showDivider = bmlProfiles.isNotEmpty() || profile != null + ) + fun updateToggleStates(saveBtn: android.widget.Button) { val visibleCount = bmlProfiles.count { it.profileId !in hidden } toggleRows.forEach { (_, toggle) -> toggle.isEnabled = !(toggle.isChecked && visibleCount == 1) } - saveBtn.isEnabled = hidden != originalHidden && visibleCount >= 1 + saveBtn.isEnabled = (hidden != originalHidden || hiddenAccounts != originalHiddenAccounts) && visibleCount >= 1 } val dialog = MaterialAlertDialogBuilder(ctx) @@ -773,8 +904,16 @@ class SettingsLoginsFragment : Fragment() { } } + accountRows.forEach { (acc, toggle) -> + toggle.setOnCheckedChangeListener { _, checked -> + if (checked) hiddenAccounts.remove(acc.accountNumber) else hiddenAccounts.add(acc.accountNumber) + updateToggleStates(saveBtn) + } + } + saveBtn.setOnClickListener { store.setHiddenBmlProfileIds(loginId, hidden) + persistHiddenAccounts(store, loginAccounts, hiddenAccounts) clearAllCaches(ctx) dialog.dismiss() (activity as? HomeActivity)?.relogin() @@ -1000,6 +1139,10 @@ class SettingsLoginsFragment : Fragment() { val dp = ctx.resources.displayMetrics.density val hide = viewModel.hideAmounts.value ?: false val masked = "••••••" + val app = requireActivity().application as BasedBankApp + val loginAccounts = app.fahipayAccounts.filter { it.loginTag == "fahipay_$loginId" } + val originalHiddenAccounts = loginAccounts.map { it.accountNumber }.filter { it in store.getHiddenAccountNumbers() }.toSet() + val hiddenAccounts = originalHiddenAccounts.toMutableSet() val scroll = android.widget.ScrollView(ctx) val container = LinearLayout(ctx).apply { @@ -1050,14 +1193,38 @@ class SettingsLoginsFragment : Fragment() { }) } - MaterialAlertDialogBuilder(ctx) + val accountRows = addAccountsSection(ctx, container, dp, loginAccounts, hiddenAccounts, showDivider = true) + + val dialog = MaterialAlertDialogBuilder(ctx) .setTitle(getString(R.string.fahipay_name)) .setView(scroll) - .setPositiveButton(R.string.close, null) - .setNegativeButton(R.string.settings_logout) { _, _ -> - confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } + .apply { + if (loginAccounts.isNotEmpty()) setPositiveButton(R.string.save, null) + setNeutralButton(R.string.close, null) + setNegativeButton(R.string.settings_logout) { _, _ -> + confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } + } } .show() + + if (loginAccounts.isNotEmpty()) { + val saveBtn = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE) + saveBtn.isEnabled = false + + accountRows.forEach { (acc, toggle) -> + toggle.setOnCheckedChangeListener { _, checked -> + if (checked) hiddenAccounts.remove(acc.accountNumber) else hiddenAccounts.add(acc.accountNumber) + saveBtn.isEnabled = hiddenAccounts != originalHiddenAccounts + } + } + + saveBtn.setOnClickListener { + persistHiddenAccounts(store, loginAccounts, hiddenAccounts) + clearAllCaches(ctx) + dialog.dismiss() + (activity as? HomeActivity)?.relogin() + } + } } private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) { @@ -1140,6 +1307,8 @@ class SettingsLoginsFragment : Fragment() { val pockets = sh.sar.basedbank.util.AccountCache.loadMfaisa(ctx, loginId) val hidden = store.getHiddenMfaisaPocketIds(loginId).toMutableSet() val originalHidden = hidden.toSet() + val originalHiddenAccounts = pockets.map { it.accountNumber }.filter { it in store.getHiddenAccountNumbers() }.toSet() + val hiddenAccounts = originalHiddenAccounts.toMutableSet() // The user-visible "profiles" are: M-Faisa (every non-PayPal pocket) and PayPal (if linked). // Each toggle covers the set of pocket account numbers that belong to that profile. @@ -1195,6 +1364,9 @@ class SettingsLoginsFragment : Fragment() { }) } + // Each group's own pockets nest directly beneath it (tree view), since a group + // ("M-Faisa" / "PayPal") can hold multiple pocket accounts. + val accountRows = mutableListOf>() val toggleRows = profileRows.map { row -> val v = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL @@ -1217,6 +1389,7 @@ class SettingsLoginsFragment : Fragment() { v.addView(label) v.addView(toggle) container.addView(v) + accountRows += addNestedAccountRows(ctx, container, dp, pockets.filter { it.accountNumber in row.pocketIds }, hiddenAccounts) row to toggle } @@ -1225,7 +1398,7 @@ class SettingsLoginsFragment : Fragment() { toggleRows.forEach { (_, toggle) -> toggle.isEnabled = !(toggle.isChecked && visibleCount == 1) } - saveBtn.isEnabled = hidden != originalHidden && visibleCount >= 1 + saveBtn.isEnabled = (hidden != originalHidden || hiddenAccounts != originalHiddenAccounts) && visibleCount >= 1 } val dialog = MaterialAlertDialogBuilder(ctx) @@ -1252,8 +1425,16 @@ class SettingsLoginsFragment : Fragment() { } } + accountRows.forEach { (acc, toggle) -> + toggle.setOnCheckedChangeListener { _, checked -> + if (checked) hiddenAccounts.remove(acc.accountNumber) else hiddenAccounts.add(acc.accountNumber) + updateToggleStates(saveBtn) + } + } + saveBtn.setOnClickListener { store.setHiddenMfaisaPocketIds(loginId, hidden) + persistHiddenAccounts(store, pockets, hiddenAccounts) clearAllCaches(ctx) dialog.dismiss() (activity as? HomeActivity)?.relogin() diff --git a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt index c06b4e5..a4e5e31 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -784,6 +784,15 @@ class CredentialStore(context: Context) { fun setHiddenMibProfileIds(loginId: String, ids: Set) = prefs.edit().putStringSet("mib_${loginId}_hidden_profile_ids", ids).apply() + // ── Per-account visibility (account numbers are globally unique) ───────── + + /** Returns the set of account numbers the user has chosen to hide, across all logins. */ + fun getHiddenAccountNumbers(): Set = + prefs.getStringSet("hidden_account_numbers", emptySet()) ?: emptySet() + + fun setHiddenAccountNumbers(accountNumbers: Set) = + prefs.edit().putStringSet("hidden_account_numbers", accountNumbers).apply() + // ── Crypto primitives ───────────────────────────────────────────────────── private fun getOrCreateKey(): SecretKey {