add dashboard and navigation menu

This commit is contained in:
2026-05-12 11:23:57 +05:00
parent 7026da9ccd
commit 4d5ff472d2
19 changed files with 613 additions and 49 deletions

View File

@@ -1,6 +1,7 @@
package sh.sar.basedbank
import android.app.Application
import androidx.appcompat.app.AppCompatDelegate
import com.google.android.material.color.DynamicColors
import sh.sar.basedbank.api.mib.MibAccount
@@ -13,5 +14,12 @@ class BasedBankApp : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
val theme = getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system")
AppCompatDelegate.setDefaultNightMode(when (theme) {
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
"light" -> AppCompatDelegate.MODE_NIGHT_NO
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
})
}
}

View File

@@ -0,0 +1,42 @@
package sh.sar.basedbank.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentAccountsBinding
class AccountsFragment : Fragment() {
private var _binding: FragmentAccountsBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
private lateinit var adapter: AccountsAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentAccountsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = AccountsAdapter(emptyList())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
viewModel.accounts.observe(viewLifecycleOwner) { adapter.updateAccounts(it) }
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_accounts)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,54 @@
package sh.sar.basedbank.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.databinding.FragmentDashboardBinding
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
val wip = { Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show() }
binding.btnTransfer.setOnClickListener { wip() }
binding.btnPayMvQr.setOnClickListener { wip() }
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_dashboard)
}
private fun updateBalances(accounts: List<MibAccount>) {
val mvrTotal = accounts
.filter { it.currencyName.equals("MVR", ignoreCase = true) }
.sumOf { it.availableBalance.replace(",", "").toDoubleOrNull() ?: 0.0 }
val usdTotal = accounts
.filter { it.currencyName.equals("USD", ignoreCase = true) }
.sumOf { it.availableBalance.replace(",", "").toDoubleOrNull() ?: 0.0 }
binding.tvMvrBalance.text = "MVR %,.2f".format(mvrTotal)
binding.tvUsdBalance.text = "USD %,.2f".format(usdTotal)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,23 +1,29 @@
package sh.sar.basedbank.ui.home
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding
import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.CredentialStore
class HomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityHomeBinding
private lateinit var adapter: AccountsAdapter
private val viewModel: HomeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -25,23 +31,50 @@ class HomeActivity : AppCompatActivity() {
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val toggle = ActionBarDrawerToggle(
this, binding.drawerLayout, binding.toolbar,
R.string.nav_open_drawer, R.string.nav_close_drawer
)
binding.drawerLayout.addDrawerListener(toggle)
toggle.syncState()
binding.navigationView.setNavigationItemSelectedListener { item ->
binding.drawerLayout.closeDrawers()
when (item.itemId) {
R.id.nav_dashboard -> show(DashboardFragment())
R.id.nav_add_account -> startActivity(Intent(this, LoginActivity::class.java))
R.id.nav_accounts -> show(AccountsFragment())
R.id.nav_settings -> show(SettingsFragment())
else -> Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
true
}
// Load data
val app = application as BasedBankApp
// If we arrived here from a fresh manual login, accounts are already in memory.
// Otherwise load the last-known cache so the UI is instant.
val initial = if (app.accounts.isNotEmpty()) app.accounts else AccountCache.load(this)
adapter = AccountsAdapter(initial)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
if (app.accounts.isNotEmpty()) {
// Just logged in — persist the fresh data and we're done
// Came from fresh manual login
viewModel.accounts.value = app.accounts
AccountCache.save(this, app.accounts)
} else {
// Came from lock screen — refresh in background
// Came from lock screen — show cache immediately, refresh in background
val cached = AccountCache.load(this)
if (cached.isNotEmpty()) viewModel.accounts.value = cached
val creds = CredentialStore(this).loadMibCredentials()
if (creds != null) autoRefresh(creds)
}
// Show dashboard on first create
if (savedInstanceState == null) {
show(DashboardFragment())
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
}
}
private fun show(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.replace(R.id.contentFrame, fragment)
.commit()
}
private fun autoRefresh(creds: CredentialStore.MibCredentials) {
@@ -55,9 +88,9 @@ class HomeActivity : AppCompatActivity() {
}
(application as BasedBankApp).accounts = accounts
AccountCache.save(this@HomeActivity, accounts)
adapter.updateAccounts(accounts)
viewModel.accounts.postValue(accounts)
} catch (_: Exception) {
// Keep showing cached data silently
// Keep cached data silently
} finally {
binding.refreshIndicator.visibility = View.GONE
}

View File

@@ -0,0 +1,9 @@
package sh.sar.basedbank.ui.home
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import sh.sar.basedbank.api.mib.MibAccount
class HomeViewModel : ViewModel() {
val accounts = MutableLiveData<List<MibAccount>>(emptyList())
}

View File

@@ -0,0 +1,56 @@
package sh.sar.basedbank.ui.home
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentSettingsBinding
class SettingsFragment : Fragment() {
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
// Set initial selection
val saved = prefs.getString("theme", "system")
val initialId = when (saved) {
"light" -> R.id.btnThemeLight
"dark" -> R.id.btnThemeDark
else -> R.id.btnThemeSystem
}
binding.themeToggle.check(initialId)
binding.themeToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked) return@addOnButtonCheckedListener
val (key, mode) = when (checkedId) {
R.id.btnThemeLight -> "light" to AppCompatDelegate.MODE_NIGHT_NO
R.id.btnThemeDark -> "dark" to AppCompatDelegate.MODE_NIGHT_YES
else -> "system" to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
prefs.edit().putString("theme", key).apply()
AppCompatDelegate.setDefaultNightMode(mode)
}
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_settings)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -114,8 +114,9 @@ class CredentialsFragment : Fragment() {
CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed)
AccountCache.save(requireContext(), accounts)
(requireActivity().application as BasedBankApp).accounts = accounts
startActivity(Intent(requireContext(), HomeActivity::class.java))
requireActivity().finish()
val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Login failed: ${e.message}", e)
binding.tvError.text = e.message ?: "Login failed"