diff --git a/app/src/debug/res/drawable/ic_launcher_background.xml b/app/src/debug/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..4cb08a7
--- /dev/null
+++ b/app/src/debug/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 0000000..e1ab9be
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Thijooree Debug
+
diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsClient.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsClient.kt
index d685eb1..c87a887 100644
--- a/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsClient.kt
+++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsClient.kt
@@ -61,38 +61,51 @@ class MibContactsClient {
}
}
+ /** Fetch a single page of contacts. Returns the list and the server-reported total count.
+ * [start] is 1-based; [count] is the desired page size. */
+ fun fetchContactsPage(
+ session: MibSession,
+ start: Int,
+ count: Int
+ ): Pair, Int> {
+ val end = start + count - 1
+ // Server uses 100-row pages internally for its own pagination; we mirror that
+ // by computing a page index that aligns with our start offset.
+ val page = ((start - 1) / 100) + 1
+ val body = FormBody.Builder()
+ .add("page", page.toString())
+ .add("search", "")
+ .add("searchCategoryId", "0")
+ .add("benefType", "A")
+ .add("sortBenef", "name")
+ .add("sortDir", "asc")
+ .add("start", start.toString())
+ .add("end", end.toString())
+ .add("includeCount", "1")
+ .build()
+
+ val request = Request.Builder()
+ .url("$BASE_WV_URL/ajaxBeneficiary/main")
+ .post(body)
+ .withSessionHeaders(session)
+ .build()
+
+ return client.newCall(request).execute().use { response ->
+ if (!response.isSuccessful) return Pair(emptyList(), 0)
+ parseContacts(response.body?.string() ?: return Pair(emptyList(), 0))
+ }
+ }
+
+ /** Convenience: fetch all pages and return the union. Used by callers that don't paginate. */
fun fetchContacts(session: MibSession): List {
val all = mutableListOf()
- var page = 1
+ var start = 1
val pageSize = 100
while (true) {
- val start = (page - 1) * pageSize + 1
- val end = page * pageSize
- val body = FormBody.Builder()
- .add("page", page.toString())
- .add("search", "")
- .add("searchCategoryId", "0")
- .add("benefType", "A")
- .add("sortBenef", "name")
- .add("sortDir", "asc")
- .add("start", start.toString())
- .add("end", end.toString())
- .add("includeCount", "1")
- .build()
-
- val request = Request.Builder()
- .url("$BASE_WV_URL/ajaxBeneficiary/main")
- .post(body)
- .withSessionHeaders(session)
- .build()
-
- val (contacts, totalCount) = client.newCall(request).execute().use { response ->
- if (!response.isSuccessful) return all
- parseContacts(response.body?.string() ?: return all)
- }
+ val (contacts, totalCount) = fetchContactsPage(session, start, pageSize)
all.addAll(contacts)
if (all.size >= totalCount || contacts.isEmpty()) break
- page++
+ start += pageSize
}
return all
}
diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsPaginator.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsPaginator.kt
new file mode 100644
index 0000000..d8b805e
--- /dev/null
+++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsPaginator.kt
@@ -0,0 +1,80 @@
+package sh.sar.basedbank.api.mib
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import sh.sar.basedbank.BasedBankApp
+import sh.sar.basedbank.api.models.BankContact
+import sh.sar.basedbank.api.models.BankContactCategory
+
+/**
+ * Streams MIB contacts one page at a time, walking the user's MIB profiles in order.
+ *
+ * For multi-profile users we stay on the current profile until its server-reported
+ * `total_count` is reached, then advance to the next profile. Each profile switch is
+ * protected by the global `mibMutex` to keep the session state coherent.
+ *
+ * Categories are fetched lazily on the first page of each profile and accumulated
+ * across calls — the latest snapshot is exposed via [allCategories].
+ */
+class MibContactsPaginator(
+ private val app: BasedBankApp,
+ val loginId: String,
+ private val session: MibSession,
+ private val profiles: List,
+ private val client: MibContactsClient = MibContactsClient()
+) {
+ private var profileIndex = 0
+ private var nextStart = 1
+ private var totalForCurrentProfile = -1
+ private var categoriesFetchedForProfile = false
+
+ private val accumulatedCategories = LinkedHashMap()
+ val allCategories: List
+ get() = accumulatedCategories.values.toList()
+
+ fun hasMore(): Boolean = profileIndex < profiles.size
+
+ /** Fetch one page of up to [count] contacts. Returns the contacts contributed by
+ * this call (may be empty if a profile errored or returned no rows). Advances
+ * the internal cursor; subsequent calls continue from where this left off. */
+ suspend fun nextPage(count: Int): List = withContext(Dispatchers.IO) {
+ if (!hasMore()) return@withContext emptyList()
+
+ val profile = profiles[profileIndex]
+ val flow = app.mibFlowFor(loginId)
+
+ val contacts = app.mibMutex.withLock {
+ try {
+ flow.switchProfile(session, profile)
+ if (!categoriesFetchedForProfile) {
+ for (cat in client.fetchCategories(session)) {
+ accumulatedCategories.putIfAbsent(
+ cat.id,
+ BankContactCategory(cat.id, cat.categoryName, cat.numBenef)
+ )
+ }
+ categoriesFetchedForProfile = true
+ }
+ val (page, total) = client.fetchContactsPage(session, nextStart, count)
+ if (total > 0) totalForCurrentProfile = total
+ nextStart += page.size.coerceAtLeast(count)
+ page.map { it.copy(profileId = profile.profileId) }
+ } catch (_: Exception) {
+ // Bail on this profile and advance — keeps the paginator robust.
+ emptyList()
+ }
+ }
+
+ val exhausted =
+ (totalForCurrentProfile > 0 && nextStart - 1 >= totalForCurrentProfile) ||
+ contacts.isEmpty()
+ if (exhausted) {
+ profileIndex++
+ nextStart = 1
+ totalForCurrentProfile = -1
+ categoriesFetchedForProfile = false
+ }
+ contacts
+ }
+}
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt
index 957c1bb..99120de 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt
@@ -8,6 +8,7 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
@@ -20,7 +21,7 @@ class ContactsAdapter(
private val onImageNeeded: (hash: String) -> Unit,
private val onDeleteClick: (ContactDisplay) -> Unit,
private val onTransferClick: (ContactDisplay) -> Unit
-) : RecyclerView.Adapter() {
+) : RecyclerView.Adapter() {
private var allContacts: List = emptyList()
private var displayed: List = emptyList()
@@ -28,6 +29,15 @@ class ContactsAdapter(
private var activeCategoryId: String? = null
private var searchQuery: String = ""
+ /** When true, a loading footer row is shown after the last contact. */
+ var showLoadingFooter: Boolean = false
+ set(value) {
+ if (field == value) return
+ field = value
+ if (value) notifyItemInserted(displayed.size)
+ else notifyItemRemoved(displayed.size)
+ }
+
fun updateContacts(contacts: List) {
allContacts = contacts
applyFilter()
@@ -58,24 +68,32 @@ class ContactsAdapter(
notifyDataSetChanged()
}
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ override fun getItemViewType(position: Int): Int =
+ if (position == displayed.size && showLoadingFooter) VIEW_TYPE_FOOTER else VIEW_TYPE_CONTACT
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ if (viewType == VIEW_TYPE_FOOTER) {
+ val footer = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_loading_footer, parent, false)
+ return FooterHolder(footer)
+ }
val binding = ItemContactBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = ViewHolder(binding)
binding.btnTransferContact.setOnClickListener {
val pos = holder.bindingAdapterPosition
- if (pos != RecyclerView.NO_POSITION) onTransferClick(displayed[pos])
+ if (pos != RecyclerView.NO_POSITION && pos < displayed.size) onTransferClick(displayed[pos])
}
binding.btnEditContact.setOnClickListener {
Toast.makeText(it.context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
binding.btnDeleteContact.setOnClickListener {
val pos = holder.bindingAdapterPosition
- if (pos != RecyclerView.NO_POSITION) onDeleteClick(displayed[pos])
+ if (pos != RecyclerView.NO_POSITION && pos < displayed.size) onDeleteClick(displayed[pos])
}
binding.root.setOnLongClickListener {
val pos = holder.bindingAdapterPosition
- if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener false
+ if (pos == RecyclerView.NO_POSITION || pos >= displayed.size) return@setOnLongClickListener false
val account = displayed[pos].accountNumber
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
@@ -86,15 +104,23 @@ class ContactsAdapter(
return holder
}
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (holder is FooterHolder) return
val contact = displayed[position]
val cachedImage = contact.imageHash?.let { hash ->
imageCache[hash] ?: run { onImageNeeded(hash); null }
}
- holder.bind(contact, cachedImage)
+ (holder as ViewHolder).bind(contact, cachedImage)
}
- override fun getItemCount() = displayed.size
+ override fun getItemCount() = displayed.size + if (showLoadingFooter) 1 else 0
+
+ private class FooterHolder(view: View) : RecyclerView.ViewHolder(view)
+
+ private companion object {
+ const val VIEW_TYPE_CONTACT = 0
+ const val VIEW_TYPE_FOOTER = 1
+ }
inner class ViewHolder(val binding: ItemContactBinding) :
RecyclerView.ViewHolder(binding.root) {
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt
index fb0897a..32cdf66 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt
@@ -76,6 +76,9 @@ class ContactsFragment : Fragment() {
fun updateImage(hash: String, bitmap: Bitmap) =
contactAdapters.forEach { it.updateImage(hash, bitmap) }
+ fun setLoadingFooter(visible: Boolean) =
+ contactAdapters.forEach { it.showLoadingFooter = visible }
+
inner class PageHolder(val rv: RecyclerView) : RecyclerView.ViewHolder(rv)
override fun getItemCount() = pages.size
@@ -93,6 +96,7 @@ class ContactsFragment : Fragment() {
val p80 = (65 * density).toInt()
setPadding(0, p4, 0, p80)
adapter = contactAdapters[viewType]
+ addOnScrollListener(scrollListener)
}
return PageHolder(rv)
}
@@ -100,6 +104,24 @@ class ContactsFragment : Fragment() {
override fun onBindViewHolder(holder: PageHolder, position: Int) {}
}
+ /** Triggers paginated MIB contact loading when the user scrolls within
+ * LOAD_MORE_THRESHOLD rows of the end of any tab. */
+ private val scrollListener = object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
+ if (dy <= 0) return
+ val lm = rv.layoutManager as? LinearLayoutManager ?: return
+ val total = lm.itemCount
+ if (total == 0) return
+ if (lm.findLastVisibleItemPosition() >= total - LOAD_MORE_THRESHOLD) {
+ (activity as? HomeActivity)?.loadMoreMibContacts()
+ }
+ }
+ }
+
+ private companion object {
+ const val LOAD_MORE_THRESHOLD = 5
+ }
+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentContactsBinding.inflate(inflater, container, false)
return binding.root
@@ -149,6 +171,15 @@ class ContactsFragment : Fragment() {
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
binding.loadingView.visibility = View.GONE
}
+
+ // Show a loading footer on each contact list while more MIB pages are arriving.
+ val updateFooter: () -> Unit = {
+ val loading = viewModel.mibContactsLoading.value == true
+ val hasMore = viewModel.mibContactsHasMore.value == true
+ pagerAdapter.setLoadingFooter(loading && hasMore)
+ }
+ viewModel.mibContactsLoading.observe(viewLifecycleOwner) { updateFooter() }
+ viewModel.mibContactsHasMore.observe(viewLifecycleOwner) { updateFooter() }
}
private fun attachMediator(pages: List) {
@@ -165,6 +196,10 @@ class ContactsFragment : Fragment() {
}
val savedPosition = binding.viewPager.currentItem
pagerAdapter = ContactsPagerAdapter(pages)
+ pagerAdapter.setLoadingFooter(
+ (viewModel.mibContactsLoading.value == true) &&
+ (viewModel.mibContactsHasMore.value == true)
+ )
binding.viewPager.adapter = pagerAdapter
attachMediator(pages)
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt
index c33bf9f..ba2d96d 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt
@@ -126,6 +126,12 @@ class DashboardFragment : Fragment() {
private fun refreshQuickActions() {
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
+ val isBottom = prefs.getBoolean("bottom_nav", false)
+ if (isBottom) {
+ binding.buttonBar.visibility = View.GONE
+ return
+ }
+ binding.buttonBar.visibility = View.VISIBLE
val ids = NavCustomization.getQuickActions(prefs)
listOf(binding.btnQuickAction1, binding.btnQuickAction2).forEachIndexed { i, btn ->
val def = NavCustomization.ALL_SWAPPABLE.find { it.id == ids[i] }
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 96219fe..1acd462 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
@@ -57,6 +57,7 @@ import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient
+import sh.sar.basedbank.api.mib.MibContactsPaginator
import sh.sar.basedbank.api.mib.MibCardsClient
import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.api.mib.MibProfile
@@ -73,6 +74,10 @@ class HomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityHomeBinding
private val viewModel: HomeViewModel by viewModels()
+
+ /** One paginator per MIB login. Reset on every refresh. */
+ private var mibContactPaginators: List = emptyList()
+ private var mibLoadMoreInFlight = false
private lateinit var toggle: ActionBarDrawerToggle
private var suppressBottomNavCallback = false
@@ -923,44 +928,78 @@ fun applyNavLabelVisibility() {
return result
}
+ /** Spin up (or replace) the paginator for this MIB login and fetch the first page.
+ * Subsequent pages arrive via [loadMoreMibContacts] in response to user scrolling. */
private fun refreshContacts(loginId: String, session: MibSession, profiles: List) {
if (profiles.isEmpty()) return
- val flow = (application as BasedBankApp).mibFlowFor(loginId)
- val contactsClient = MibContactsClient()
+ val paginator = MibContactsPaginator(
+ app = application as BasedBankApp,
+ loginId = loginId,
+ session = session,
+ profiles = profiles
+ )
+ // Replace any existing paginator for this login.
+ mibContactPaginators = mibContactPaginators.filter { it.loginId != loginId } + paginator
+ viewModel.mibContactsHasMore.value = mibContactPaginators.any { it.hasMore() }
+
lifecycleScope.launch {
- try {
- val (allContacts, allCategories) = withContext(Dispatchers.IO) {
- val seenContacts = mutableSetOf()
- val seenCategories = mutableSetOf()
- val contacts = mutableListOf()
- val categories = mutableListOf()
- for (profile in profiles) {
- try {
- flow.switchProfile(session, profile)
- for (cat in contactsClient.fetchCategories(session)) {
- if (seenCategories.add(cat.id)) categories.add(cat)
- }
- for (contact in contactsClient.fetchContacts(session)) {
- if (seenContacts.add(contact.benefNo))
- contacts.add(contact.copy(profileId = profile.profileId))
- }
- } catch (_: Exception) { /* profile has no contacts access */ }
- }
- Pair(contacts, categories)
- }
- if (allContacts.isNotEmpty()) {
- ContactsCache.save(this@HomeActivity, allContacts, allCategories)
- val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity)
- val bmlContacts = ContactsCache.loadBml(this@HomeActivity, store.getBmlLoginIds())
- val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
- val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
- viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
- viewModel.contactCategories.postValue(allCategories + fahipayCategories)
- }
- } catch (_: Exception) { /* keep cached data */ }
+ viewModel.mibContactsLoading.postValue(true)
+ val firstPage = try {
+ paginator.nextPage(MIB_FIRST_PAGE_SIZE)
+ } catch (_: Exception) {
+ null
+ }
+ // Treat a successful refresh as authoritative: replace the cached MIB list
+ // with the first page, even if it shrinks (so deletions take effect).
+ if (firstPage != null) {
+ ContactsCache.save(this@HomeActivity, firstPage, paginator.allCategories)
+ publishContacts(paginator.allCategories)
+ }
+ viewModel.mibContactsLoading.postValue(false)
+ viewModel.mibContactsHasMore.postValue(mibContactPaginators.any { it.hasMore() })
}
}
+ /** Called by ContactsFragment when the user scrolls near the end of the loaded list. */
+ fun loadMoreMibContacts() {
+ if (mibLoadMoreInFlight) return
+ val paginator = mibContactPaginators.firstOrNull { it.hasMore() } ?: return
+ mibLoadMoreInFlight = true
+ lifecycleScope.launch {
+ viewModel.mibContactsLoading.postValue(true)
+ val page = try {
+ paginator.nextPage(MIB_NEXT_PAGE_SIZE)
+ } catch (_: Exception) {
+ emptyList()
+ }
+ if (page.isNotEmpty()) {
+ ContactsCache.appendContacts(this@HomeActivity, page, paginator.allCategories)
+ publishContacts(paginator.allCategories)
+ }
+ mibLoadMoreInFlight = false
+ viewModel.mibContactsLoading.postValue(false)
+ viewModel.mibContactsHasMore.postValue(mibContactPaginators.any { it.hasMore() })
+ }
+ }
+
+ /** Merge the current MIB cache with BML and Fahipay caches and post to the view model. */
+ private fun publishContacts(mibCategories: List) {
+ val store = sh.sar.basedbank.util.CredentialStore(this)
+ val mibContacts = ContactsCache.loadContacts(this)
+ val bmlContacts = ContactsCache.loadBml(this, store.getBmlLoginIds())
+ val fahipayContacts = ContactsCache.loadFahipay(this)
+ val fahipayCategories = ContactsCache.loadFahipayCategories(this)
+ viewModel.contacts.postValue(
+ mergeContacts(mergeContacts(mibContacts, bmlContacts), fahipayContacts)
+ )
+ viewModel.contactCategories.postValue(mibCategories + fahipayCategories)
+ }
+
+ companion object {
+ private const val MIB_FIRST_PAGE_SIZE = 30
+ private const val MIB_NEXT_PAGE_SIZE = 100
+ }
+
fun refreshBalances(src: BankAccount) {
val app = application as BasedBankApp
lifecycleScope.launch {
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt
index 298eebc..24fc2bb 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt
@@ -23,6 +23,11 @@ class HomeViewModel : ViewModel() {
val contacts = MutableLiveData>(emptyList())
val contactCategories = MutableLiveData>(emptyList())
+ /** True while MIB contacts are paging in (initial load or scroll-triggered next page). */
+ val mibContactsLoading = MutableLiveData(false)
+ /** True while any MIB paginator still has pages to deliver. */
+ val mibContactsHasMore = MutableLiveData(false)
+
data class BmlLimitsData(val userName: String, val limits: List)
val bmlLimits = MutableLiveData>(emptyList())
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt
index 778f3d9..0579c26 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt
@@ -54,19 +54,29 @@ class SettingsAppearanceFragment : Fragment() {
// Quick actions
quickActions.clear()
quickActions.addAll(NavCustomization.getQuickActions(prefs))
- quickActionAdapter = NavItemAdapter(quickActions) {
- NavCustomization.saveQuickActions(prefs, quickActions)
+ quickActionAdapter = NavItemAdapter(
+ items = quickActions,
+ onSave = { NavCustomization.saveQuickActions(prefs, quickActions) },
+ isEnabled = { !prefs.getBoolean("bottom_nav", false) }
+ )
+ setupNavItemRecyclerView(binding.rvQuickActions, quickActionAdapter, quickActions) {
+ !prefs.getBoolean("bottom_nav", false)
}
- setupNavItemRecyclerView(binding.rvQuickActions, quickActionAdapter, quickActions)
// Bottom bar shortcuts
slots.clear()
slots.addAll(NavCustomization.getSlots(prefs))
- slotAdapter = NavItemAdapter(slots) {
- NavCustomization.saveSlots(prefs, slots)
- (activity as? HomeActivity)?.rebuildBottomNav(prefs)
+ slotAdapter = NavItemAdapter(
+ items = slots,
+ onSave = {
+ NavCustomization.saveSlots(prefs, slots)
+ (activity as? HomeActivity)?.rebuildBottomNav(prefs)
+ },
+ isEnabled = { prefs.getBoolean("bottom_nav", false) }
+ )
+ setupNavItemRecyclerView(binding.rvNavSlots, slotAdapter, slots) {
+ prefs.getBoolean("bottom_nav", false)
}
- setupNavItemRecyclerView(binding.rvNavSlots, slotAdapter, slots)
// Show labels toggle
val showLabels = prefs.getBoolean("bottom_nav_show_labels", true)
binding.switchShowLabels.isChecked = showLabels
@@ -109,13 +119,18 @@ class SettingsAppearanceFragment : Fragment() {
private fun setupNavItemRecyclerView(
rv: RecyclerView,
adapter: NavItemAdapter,
- items: MutableList
+ items: MutableList,
+ isEnabled: () -> Boolean
) {
rv.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
rv.adapter = adapter
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.START or ItemTouchHelper.END, 0
) {
+ override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
+ if (!isEnabled()) return 0
+ return super.getMovementFlags(recyclerView, viewHolder)
+ }
override fun onMove(
rv: RecyclerView,
from: RecyclerView.ViewHolder,
@@ -134,11 +149,17 @@ class SettingsAppearanceFragment : Fragment() {
private fun updateShortcutsVisibility() {
val isBottom = prefs.getBoolean("bottom_nav", false)
+ binding.sectionQuickActions.alpha = if (isBottom) 0.38f else 1f
binding.sectionBottomBarShortcuts.alpha = if (isBottom) 1f else 0.38f
+ binding.switchShowLabels.isClickable = isBottom
+ quickActionAdapter.notifyDataSetChanged()
+ slotAdapter.notifyDataSetChanged()
}
private fun showItemPicker(items: MutableList, slotIndex: Int, adapter: NavItemAdapter) {
- if (items === slots && !prefs.getBoolean("bottom_nav", false)) return
+ val isBottom = prefs.getBoolean("bottom_nav", false)
+ if (items === slots && !isBottom) return
+ if (items === quickActions && isBottom) return
val ctx = requireContext()
val otherIds = items.filterIndexed { i, _ -> i != slotIndex }.toSet()
val available = NavCustomization.ALL_SWAPPABLE.filter { it.id !in otherIds }
@@ -169,7 +190,8 @@ class SettingsAppearanceFragment : Fragment() {
private inner class NavItemAdapter(
val items: MutableList,
- val onSave: () -> Unit
+ val onSave: () -> Unit,
+ val isEnabled: () -> Boolean = { true }
) : RecyclerView.Adapter() {
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
@@ -191,7 +213,12 @@ class SettingsAppearanceFragment : Fragment() {
val def = NavCustomization.ALL_SWAPPABLE.find { it.id == items[position] } ?: return
holder.ivNavIcon.setImageResource(def.iconRes)
holder.tvNavLabel.setText(def.titleRes)
- holder.itemView.setOnClickListener { showItemPicker(items, holder.adapterPosition, this) }
+ val enabled = isEnabled()
+ holder.itemView.setOnClickListener(
+ if (enabled) View.OnClickListener { showItemPicker(items, holder.adapterPosition, this) }
+ else null
+ )
+ holder.itemView.isClickable = enabled
}
}
diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt
index 148cd38..562ef4f 100644
--- a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt
+++ b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt
@@ -51,6 +51,21 @@ object ContactsCache {
prefs.apply()
}
+ /** Merge new MIB contacts into the cached list, deduping by benefNo. Categories
+ * are replaced wholesale (the latest snapshot is canonical). Safe to call
+ * repeatedly as pages arrive. */
+ fun appendContacts(
+ context: Context,
+ newContacts: List,
+ categories: List
+ ) {
+ if (newContacts.isEmpty() && categories.isEmpty()) return
+ val existing = loadContacts(context)
+ val seen = existing.map { it.benefNo }.toHashSet()
+ val merged = existing + newContacts.filter { seen.add(it.benefNo) }
+ save(context, merged, categories)
+ }
+
fun clear(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
}
diff --git a/app/src/main/res/layout/fragment_settings_appearance.xml b/app/src/main/res/layout/fragment_settings_appearance.xml
index 6cddf24..12d6416 100644
--- a/app/src/main/res/layout/fragment_settings_appearance.xml
+++ b/app/src/main/res/layout/fragment_settings_appearance.xml
@@ -45,22 +45,30 @@
-
-
-
-
+
+ android:orientation="vertical"
+ android:layout_marginBottom="16dp">
+
+
+
+
+
+