Basically rewrite most of the app
A lot of stuff was moved around and some of the API connections were improved upon. It's still in a pretty broken state but it's minimally functional enough for day-to-day use for myself
This commit is contained in:
parent
3dd98b1e08
commit
6f24feed7b
81 changed files with 2033 additions and 972 deletions
|
@ -20,17 +20,14 @@ android {
|
|||
useSupportLibrary = true
|
||||
}
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
buildConfigField "String", "API_URL", "\"https://budget-api.projects.wbrawner.com/\""
|
||||
buildConfigField "String", "API_URL", "\"http://192.168.86.92:8080/\""
|
||||
// buildConfigField "String", "API_URL", "\"http://10.0.2.2:8080/\""
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
buildConfigField "String", "API_URL", "\"https://budget-api.intra.wbrawner.com/\""
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
|
@ -45,17 +42,16 @@ dependencies {
|
|||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.core:core:1.0.2'
|
||||
implementation 'androidx.media:media:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core:1.1.0'
|
||||
implementation 'androidx.media:media:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha10'
|
||||
implementation 'androidx.emoji:emoji-bundled:1.0.0'
|
||||
implementation 'com.github.BlacKCaT27:CurrencyEditText:2.0.2'
|
||||
def lifecycle_version = "1.1.1"
|
||||
// ViewModel and LiveData
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
// Dagger
|
||||
implementation "com.google.dagger:dagger:$rootProject.ext.dagger"
|
||||
kapt "com.google.dagger:dagger-compiler:$rootProject.ext.dagger"
|
||||
|
@ -64,8 +60,23 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
implementation 'com.google.firebase:firebase-core:16.0.9'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
|
||||
|
||||
implementation 'com.google.firebase:firebase-core:17.2.0'
|
||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
|
||||
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-core:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-attr:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-build-config:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-crash:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-disk:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-geiger-counter:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-measurement:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-phoenix:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-recorder:$rootProject.ext.hyperion"
|
||||
debugImplementation "com.willowtreeapps.hyperion:hyperion-shared-preferences:$rootProject.ext.hyperion"
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity
|
||||
android:name=".ui.SplashActivity"
|
||||
|
|
|
@ -6,10 +6,7 @@ import com.wbrawner.budget.di.AppComponent
|
|||
import com.wbrawner.budget.di.DaggerAppComponent
|
||||
|
||||
class AllowanceApplication : Application() {
|
||||
var currentUser: User? = User(
|
||||
id = 1,
|
||||
username = "wbrawner"
|
||||
)
|
||||
var currentUser: User? = null
|
||||
lateinit var appComponent: AppComponent
|
||||
private set
|
||||
|
||||
|
|
|
@ -6,7 +6,14 @@ import com.wbrawner.budget.lib.network.NetworkModule
|
|||
import com.wbrawner.budget.ui.MainActivity
|
||||
import com.wbrawner.budget.ui.SplashActivity
|
||||
import com.wbrawner.budget.ui.SplashViewModelMapper
|
||||
import com.wbrawner.budget.ui.auth.LoginFragment
|
||||
import com.wbrawner.budget.ui.auth.RegisterFragment
|
||||
import com.wbrawner.budget.ui.budgets.AddEditAccountsViewModelMapper
|
||||
import com.wbrawner.budget.ui.budgets.AddEditBudgetFragment
|
||||
import com.wbrawner.budget.ui.budgets.BudgetListFragment
|
||||
import com.wbrawner.budget.ui.budgets.BudgetViewModelMapper
|
||||
import com.wbrawner.budget.ui.categories.AddEditCategoryActivity
|
||||
import com.wbrawner.budget.ui.categories.CategoryFragment
|
||||
import com.wbrawner.budget.ui.categories.CategoryListFragment
|
||||
import com.wbrawner.budget.ui.categories.CategoryViewModelMapper
|
||||
import com.wbrawner.budget.ui.overview.AccountOverviewViewModelMapper
|
||||
|
@ -25,6 +32,11 @@ import javax.inject.Singleton
|
|||
@Component(modules = [AuthModule::class, AppModule::class, NetworkModule::class])
|
||||
interface AppComponent {
|
||||
fun inject(fragment: OverviewFragment)
|
||||
fun inject(fragment: LoginFragment)
|
||||
fun inject(fragment: BudgetListFragment)
|
||||
fun inject(fragment: RegisterFragment)
|
||||
fun inject(fragment: AddEditBudgetFragment)
|
||||
fun inject(fragment: CategoryFragment)
|
||||
fun inject(fragment: TransactionListFragment)
|
||||
fun inject(fragment: CategoryListFragment)
|
||||
fun inject(activity: MainActivity)
|
||||
|
@ -46,7 +58,9 @@ interface AppComponent {
|
|||
@Module(
|
||||
includes = [
|
||||
AccountOverviewViewModelMapper::class,
|
||||
AddEditAccountsViewModelMapper::class,
|
||||
AddEditTransactionViewModelMapper::class,
|
||||
BudgetViewModelMapper::class,
|
||||
CategoryViewModelMapper::class,
|
||||
SplashViewModelMapper::class,
|
||||
TransactionListViewModelMapper::class,
|
||||
|
|
5
app/src/main/java/com/wbrawner/budget/ui/Constants.kt
Normal file
5
app/src/main/java/com/wbrawner/budget/ui/Constants.kt
Normal file
|
@ -0,0 +1,5 @@
|
|||
package com.wbrawner.budget.ui
|
||||
|
||||
const val EXTRA_BUDGET_ID = "budgetId"
|
||||
const val EXTRA_CATEGORY_ID = "categoryId"
|
||||
const val EXTRA_TRANSACTION_ID = "transactionId"
|
|
@ -1,76 +1,46 @@
|
|||
package com.wbrawner.budget.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.ui.categories.CategoryListFragment
|
||||
import com.wbrawner.budget.ui.overview.OverviewFragment
|
||||
import com.wbrawner.budget.ui.transactions.TransactionListFragment
|
||||
import kotlinx.android.synthetic.main.activity_transaction_list.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private val account = Account(
|
||||
id = 2,
|
||||
name = "Wells Fargo Checking",
|
||||
currencyCode = "USD"
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
EmojiCompat.init(androidx.emoji.bundled.BundledEmojiCompatConfig(this))
|
||||
setContentView(R.layout.activity_transaction_list)
|
||||
setSupportActionBar(action_bar)
|
||||
|
||||
menu_main.setOnNavigationItemSelectedListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_transactions -> updateFragment(
|
||||
TransactionListFragment.TAG_FRAGMENT,
|
||||
TransactionListFragment.TITLE_FRAGMENT
|
||||
)
|
||||
R.id.action_categories -> updateFragment(
|
||||
CategoryListFragment.TAG_FRAGMENT,
|
||||
CategoryListFragment.TITLE_FRAGMENT
|
||||
)
|
||||
else ->
|
||||
updateFragment(OverviewFragment.TAG_FRAGMENT, OverviewFragment.TITLE_FRAGMENT)
|
||||
val navController = findNavController(R.id.content_container)
|
||||
menu_main.setupWithNavController(navController)
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
title = destination.label
|
||||
val showHomeAsUp = when (destination.label) {
|
||||
getString(R.string.title_transactions) -> false
|
||||
getString(R.string.title_profile) -> false
|
||||
getString(R.string.title_budgets) -> false
|
||||
else -> true
|
||||
}
|
||||
true
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(showHomeAsUp)
|
||||
}
|
||||
|
||||
val openFragmentId = when (intent?.getStringExtra(EXTRA_OPEN_FRAGMENT)) {
|
||||
TransactionListFragment.TAG_FRAGMENT -> R.id.action_transactions
|
||||
CategoryListFragment.TAG_FRAGMENT -> R.id.action_categories
|
||||
else -> R.id.action_overview
|
||||
CategoryListFragment.TAG_FRAGMENT -> R.id.categoryListFragment
|
||||
else -> R.id.transactionListFragment
|
||||
}
|
||||
|
||||
menu_main.selectedItemId = openFragmentId
|
||||
}
|
||||
|
||||
private fun updateFragment(tag: String, @StringRes title: Int) {
|
||||
setTitle(title)
|
||||
var fragment = supportFragmentManager.findFragmentByTag(tag)
|
||||
val ft = supportFragmentManager.beginTransaction()
|
||||
if (fragment == null) {
|
||||
fragment = when (tag) {
|
||||
OverviewFragment.TAG_FRAGMENT -> OverviewFragment().apply { account = this@MainActivity.account }
|
||||
CategoryListFragment.TAG_FRAGMENT -> CategoryListFragment().apply { account = this@MainActivity.account }
|
||||
else -> {
|
||||
TransactionListFragment().apply { account = this@MainActivity.account }
|
||||
}
|
||||
}
|
||||
ft.add(R.id.content_container, fragment, tag)
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
if (item?.itemId == android.R.id.home) {
|
||||
findNavController(R.id.content_container).navigateUp()
|
||||
return true
|
||||
}
|
||||
for (fmFragment in supportFragmentManager.fragments) {
|
||||
if (fmFragment == fragment) {
|
||||
ft.show(fmFragment)
|
||||
} else {
|
||||
ft.hide(fmFragment)
|
||||
}
|
||||
}
|
||||
|
||||
ft.commit()
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
package com.wbrawner.budget.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.activity_splash.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
private val disposables = CompositeDisposable()
|
||||
class SplashActivity : AppCompatActivity(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private lateinit var viewModel: SplashViewModel
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
|
@ -30,54 +32,20 @@ class SplashActivity : AppCompatActivity() {
|
|||
}
|
||||
(application as AllowanceApplication).appComponent.inject(this)
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory).get(SplashViewModel::class.java)
|
||||
viewModel.checkForExistingCredentials()
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { hasExistingCredentials, err ->
|
||||
if (hasExistingCredentials) {
|
||||
startActivity(Intent(applicationContext, MainActivity::class.java))
|
||||
finish()
|
||||
} else {
|
||||
showLogin()
|
||||
}
|
||||
val navController = findNavController(R.id.auth_content)
|
||||
launch {
|
||||
val navId = try {
|
||||
val user = viewModel.checkForExistingCredentials()
|
||||
if (user != null) {
|
||||
(application as AllowanceApplication).currentUser = user
|
||||
R.id.mainActivity
|
||||
} else {
|
||||
R.id.loginFragment
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
}
|
||||
|
||||
private fun showLogin() {
|
||||
loginContainer.show()
|
||||
viewModel.isLoading
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { isLoading ->
|
||||
formPrompt.show(!isLoading)
|
||||
usernameContainer.show(!isLoading)
|
||||
passwordContainer.show(!isLoading)
|
||||
submit.show(!isLoading)
|
||||
progressBar.show(isLoading)
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
submit.setOnClickListener {
|
||||
if (!username.ensureNotEmpty() || !password.ensureNotEmpty()) {
|
||||
return@setOnClickListener
|
||||
} catch (e: Exception) {
|
||||
R.id.loginFragment
|
||||
}
|
||||
viewModel.login(username.text.toString(), password.text.toString())
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { success, error ->
|
||||
if (error != null || !success) {
|
||||
username.error = "Invalid username/password"
|
||||
password.error = "Invalid username/password"
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
startActivity(Intent(applicationContext, MainActivity::class.java))
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
navController.navigate(navId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
disposables.dispose()
|
||||
disposables.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
package com.wbrawner.budget.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.auth.CredentialsProvider
|
||||
import com.wbrawner.budget.common.account.AccountRepository
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.user.User
|
||||
import com.wbrawner.budget.di.ViewModelKey
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import javax.inject.Inject
|
||||
|
||||
class SplashViewModel @Inject constructor(
|
||||
private val credentialsProvider: CredentialsProvider,
|
||||
private val accountRepository: AccountRepository
|
||||
private val budgetRepository: BudgetRepository
|
||||
) : ViewModel() {
|
||||
val isLoading: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false)
|
||||
val isLoading: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
|
||||
fun checkForExistingCredentials(): Single<Boolean> = Single.create { subscriber ->
|
||||
login(credentialsProvider.username(), credentialsProvider.password()).subscribe { _, err ->
|
||||
subscriber.onSuccess(err == null)
|
||||
suspend fun checkForExistingCredentials(): User? {
|
||||
return try {
|
||||
login(credentialsProvider.username, credentialsProvider.password)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun login(username: String, password: String): Single<Boolean> = Single.create { subscriber ->
|
||||
isLoading.onNext(true)
|
||||
suspend fun login(username: String, password: String): User {
|
||||
isLoading.value = true
|
||||
credentialsProvider.saveCredentials(username, password)
|
||||
accountRepository.findAll().subscribe { _, err ->
|
||||
isLoading.onNext(false)
|
||||
if (err != null) {
|
||||
subscriber.onError(err)
|
||||
} else {
|
||||
subscriber.onSuccess(true)
|
||||
}
|
||||
return try {
|
||||
budgetRepository.login(username, password)
|
||||
} catch (e: java.lang.Exception) {
|
||||
isLoading.value = false
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
package com.wbrawner.budget.ui
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.res.Resources
|
||||
import android.content.Context
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.wbrawner.budget.R
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
fun Disposable.autoDispose(compositeDisposable: CompositeDisposable) {
|
||||
compositeDisposable.add(this)
|
||||
}
|
||||
|
||||
fun RecyclerView.hideFabOnScroll(fab: FloatingActionButton) {
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
@ -27,35 +19,12 @@ fun RecyclerView.hideFabOnScroll(fab: FloatingActionButton) {
|
|||
})
|
||||
}
|
||||
|
||||
fun <T> Single<T>.fromBackgroundToMain(): Single<T> {
|
||||
return this.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.fromBackgroundToMain(): Observable<T> {
|
||||
return this.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun View.hide() {
|
||||
show(false)
|
||||
}
|
||||
|
||||
fun View.show(show: Boolean = true) {
|
||||
val shortAnimTime = Resources.getSystem().getInteger(android.R.integer.config_shortAnimTime).toLong()
|
||||
if (show) {
|
||||
// Put the view back on the screen before animating its visibility
|
||||
alpha = 0f
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
animate()
|
||||
.setDuration(shortAnimTime)
|
||||
.alpha((if (show) 1 else 0).toFloat())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
this@show.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
})
|
||||
visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun EditText.ensureNotEmpty(): Boolean {
|
||||
|
@ -67,3 +36,23 @@ fun EditText.ensureNotEmpty(): Boolean {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun Long.toAmountSpannable(context: Context? = null): Spannable {
|
||||
val spannableStringBuilder = SpannableStringBuilder()
|
||||
spannableStringBuilder.append(String.format("${'$'}%.02f", this / 100.0f))
|
||||
if (context == null) {
|
||||
return spannableStringBuilder
|
||||
}
|
||||
val color = when {
|
||||
this > 0 -> R.color.colorTextGreen
|
||||
this == 0L -> R.color.colorTextPrimary
|
||||
else -> R.color.colorTextRed
|
||||
}
|
||||
spannableStringBuilder.setSpan(
|
||||
ForegroundColorSpan(ContextCompat.getColor(context, color)),
|
||||
0,
|
||||
spannableStringBuilder.length,
|
||||
0
|
||||
)
|
||||
return spannableStringBuilder
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package com.wbrawner.budget.ui.auth
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.MainActivity
|
||||
import com.wbrawner.budget.ui.SplashViewModel
|
||||
import com.wbrawner.budget.ui.ensureNotEmpty
|
||||
import com.wbrawner.budget.ui.show
|
||||
import kotlinx.android.synthetic.main.fragment_login.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class LoginFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private lateinit var viewModel: SplashViewModel
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(requireActivity().application as AllowanceApplication)
|
||||
.appComponent
|
||||
.inject(this)
|
||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
|
||||
.get(SplashViewModel::class.java)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_login, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.isLoading.observe(this, Observer { isLoading ->
|
||||
formPrompt.show(!isLoading)
|
||||
usernameContainer.show(!isLoading)
|
||||
passwordContainer.show(!isLoading)
|
||||
submit.show(!isLoading)
|
||||
registerButton.show(!isLoading)
|
||||
forgotPasswordLink.show(!isLoading)
|
||||
progressBar.show(isLoading)
|
||||
})
|
||||
password.setOnEditorActionListener { _, _, _ ->
|
||||
submit.performClick()
|
||||
}
|
||||
submit.setOnClickListener {
|
||||
if (!username.ensureNotEmpty() || !password.ensureNotEmpty()) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
launch {
|
||||
try {
|
||||
val user = viewModel.login(username.text.toString(), password.text.toString())
|
||||
(requireActivity().application as AllowanceApplication).currentUser = user
|
||||
startActivity(Intent(requireContext().applicationContext, MainActivity::class.java))
|
||||
} catch (e: Exception) {
|
||||
username.error = "Invalid username/password"
|
||||
password.error = "Invalid username/password"
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
val usernameString = arguments?.getString(EXTRA_USERNAME)
|
||||
val passwordString = arguments?.getString(EXTRA_PASSWORD)
|
||||
if (!usernameString.isNullOrBlank() && !passwordString.isNullOrBlank()) {
|
||||
username.setText(usernameString)
|
||||
password.setText(passwordString)
|
||||
submit.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_USERNAME = "username"
|
||||
const val EXTRA_PASSWORD = "password"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.wbrawner.budget.ui.auth
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.wbrawner.budget.R
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class RegisterFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_login, container, false)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package com.wbrawner.budget.ui.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.*
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.hideFabOnScroll
|
||||
import com.wbrawner.budget.ui.show
|
||||
import kotlinx.android.synthetic.main.fragment_list_with_add_button.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class ListWithAddButtonFragment<T : LoadingViewModel> : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
lateinit var viewModel: T
|
||||
private var viewCreated = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory).get(viewModelClass)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_list_with_add_button, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.isLoading.observe(this, Observer {
|
||||
view.findViewById<ProgressBar?>(R.id.progressBar)?.show(it)
|
||||
})
|
||||
recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||
recyclerView.hideFabOnScroll(addFab)
|
||||
addFab.setOnClickListener {
|
||||
addItem()
|
||||
}
|
||||
reloadItems()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
reloadItems()
|
||||
}
|
||||
|
||||
private fun reloadItems() {
|
||||
launch {
|
||||
if (view == null) return@launch
|
||||
val (items, constructors) = loadItems()
|
||||
if (items.isEmpty()) {
|
||||
recyclerView.adapter = null
|
||||
recyclerView.visibility = View.GONE
|
||||
noItemsTextView.setText(noItemsStringRes)
|
||||
noItemsTextView.visibility = View.VISIBLE
|
||||
} else {
|
||||
recyclerView.adapter = BindableAdapter(items, constructors)
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
noItemsTextView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
try {
|
||||
coroutineContext.cancel()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
abstract val viewModelClass: Class<T>
|
||||
|
||||
@get:StringRes
|
||||
abstract val noItemsStringRes: Int
|
||||
|
||||
abstract fun addItem()
|
||||
|
||||
abstract suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>>
|
||||
}
|
||||
|
||||
abstract class LoadingViewModel : ViewModel() {
|
||||
val isLoading: LiveData<Boolean> = MutableLiveData(true)
|
||||
suspend fun <T> showLoader(block: suspend () -> T): T {
|
||||
(isLoading as MutableLiveData).postValue(true)
|
||||
val result = block.invoke()
|
||||
isLoading.postValue(false)
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package com.wbrawner.budget.ui.base
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class BindableAdapter(
|
||||
private val items: List<BindableState>,
|
||||
private val constructors: Map<Int, (view: View) -> BindableViewHolder<in BindableState>>
|
||||
) : RecyclerView.Adapter<BindableAdapter.BindableViewHolder<in BindableState>>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
|
||||
: BindableViewHolder<in BindableState> = constructors[viewType]
|
||||
?.invoke(LayoutInflater.from(parent.context).inflate(viewType, parent, false))
|
||||
?: throw IllegalStateException("Attempted to create ViewHolder without proper constructor provided")
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
override fun getItemViewType(position: Int): Int = items[position].viewType
|
||||
|
||||
override fun onBindViewHolder(holder: BindableViewHolder<in BindableState>, position: Int) {
|
||||
holder.onBind(items[position])
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: BindableViewHolder<in BindableState>) {
|
||||
holder.onUnbind()
|
||||
}
|
||||
|
||||
abstract class BindableViewHolder<T>(itemView: View)
|
||||
: RecyclerView.ViewHolder(itemView), Bindable<T>
|
||||
|
||||
abstract class CoroutineViewHolder<T>(itemView: View) : BindableViewHolder<T>(itemView), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
|
||||
override fun onUnbind() {
|
||||
try {
|
||||
coroutineContext.cancel()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
super.onUnbind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Bindable<T> {
|
||||
fun onBind(item: T) {}
|
||||
fun onUnbind() {}
|
||||
}
|
||||
|
||||
interface BindableState {
|
||||
@get:LayoutRes
|
||||
val viewType: Int
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package com.wbrawner.budget.ui.budgets
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
|
||||
import kotlinx.android.synthetic.main.fragment_add_edit_budget.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Inject
|
||||
|
||||
class AddEditBudgetFragment : Fragment() {
|
||||
lateinit var viewModel: AddEditBudgetViewModel
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
var id: Long? = null
|
||||
var menu: Menu? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(requireActivity().application as AllowanceApplication)
|
||||
.appComponent
|
||||
.inject(this)
|
||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
|
||||
.get(AddEditBudgetViewModel::class.java)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_add_edit_budget, container, false)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_add_edit, menu)
|
||||
if (id != null) {
|
||||
menu.findItem(R.id.action_delete)?.isVisible = true
|
||||
}
|
||||
this.menu = menu
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
usersSearch.setAdapter(viewModel.suggestionsAdapter)
|
||||
launch {
|
||||
val budget = try {
|
||||
viewModel.getBudget(arguments!!.getLong(EXTRA_BUDGET_ID))
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
activity?.setTitle(R.string.title_edit_budget)
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = true
|
||||
id = budget.id
|
||||
name.setText(budget.name)
|
||||
description.setText(budget.description)
|
||||
budget.users.forEach {
|
||||
viewModel.addUser(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> findNavController().navigateUp()
|
||||
R.id.action_save -> {
|
||||
launch {
|
||||
viewModel.saveBudget(Budget(
|
||||
id = id,
|
||||
name = name.text.toString(),
|
||||
description = description.text.toString(),
|
||||
users = viewModel.users.value ?: emptyList()
|
||||
))
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
launch {
|
||||
viewModel.deleteBudget(this@AddEditBudgetFragment.id!!)
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fun AddEditBudgetFragment.launch(block: suspend CoroutineScope.() -> Unit) = viewModel.launch(block)
|
|
@ -0,0 +1,64 @@
|
|||
package com.wbrawner.budget.ui.budgets
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.user.User
|
||||
import com.wbrawner.budget.common.user.UserRepository
|
||||
import com.wbrawner.budget.di.ViewModelKey
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class AddEditBudgetViewModel @Inject constructor(
|
||||
context: Context,
|
||||
private val userRepository: UserRepository,
|
||||
private val budgetRepository: BudgetRepository
|
||||
) : ViewModel() {
|
||||
val suggestionsAdapter = ArrayAdapter<String>(
|
||||
context,
|
||||
android.R.layout.simple_spinner_item,
|
||||
mutableListOf()
|
||||
)
|
||||
val users = MutableLiveData<List<User>>()
|
||||
|
||||
suspend fun getBudget(id: Long) = budgetRepository.findById(id)
|
||||
|
||||
suspend fun saveBudget(budget: Budget): Budget = if (budget.id != null) {
|
||||
budgetRepository.update(budget)
|
||||
} else {
|
||||
budgetRepository.create(budget)
|
||||
}
|
||||
|
||||
suspend fun deleteBudget(accountId: Long) = budgetRepository.delete(accountId)
|
||||
|
||||
suspend fun searchUsers(query: String) {
|
||||
suggestionsAdapter.clear()
|
||||
if (query.isNotBlank()) {
|
||||
suggestionsAdapter.addAll(userRepository.findAllByNameLike(query).map { it.username })
|
||||
}
|
||||
}
|
||||
|
||||
fun addUser(user: User) {
|
||||
users.value
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
abstract class AddEditAccountsViewModelMapper {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(AddEditBudgetViewModel::class)
|
||||
abstract fun bindViewModel(viewModel: AddEditBudgetViewModel): ViewModel
|
||||
}
|
||||
|
||||
fun ViewModel.launch(block: suspend CoroutineScope.() -> Unit) {
|
||||
viewModelScope.launch(block = block)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package com.wbrawner.budget.ui.budgets
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
|
||||
import com.wbrawner.budget.ui.base.BindableAdapter
|
||||
import com.wbrawner.budget.ui.base.BindableState
|
||||
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class BudgetListFragment : ListWithAddButtonFragment<BudgetViewModel>(), CoroutineScope {
|
||||
override val noItemsStringRes: Int = R.string.overview_no_data
|
||||
override val viewModelClass: Class<BudgetViewModel> = BudgetViewModel::class.java
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>> {
|
||||
val budgetItems = viewModel.getBudgets().map { BudgetState(it) }
|
||||
val navController = findNavController()
|
||||
return Pair(
|
||||
budgetItems,
|
||||
mapOf(BUDGET_VIEW to { v -> BudgetViewHolder(v, navController) as BindableAdapter.BindableViewHolder<in BindableState> })
|
||||
)
|
||||
}
|
||||
|
||||
override fun addItem() {
|
||||
findNavController().navigate(R.id.addEditBudget)
|
||||
}
|
||||
}
|
||||
|
||||
const val BUDGET_VIEW = R.layout.list_item_budget
|
||||
|
||||
class BudgetState(val budget: Budget) : BindableState {
|
||||
override val viewType: Int = BUDGET_VIEW
|
||||
}
|
||||
|
||||
class BudgetViewHolder(
|
||||
itemView: View,
|
||||
private val navController: NavController
|
||||
) : BindableAdapter.BindableViewHolder<BudgetState>(itemView) {
|
||||
private val name: TextView = itemView.findViewById(R.id.budgetName)
|
||||
private val description: TextView = itemView.findViewById(R.id.budgetDescription)
|
||||
// private val balance: TextView = itemView.findViewById(R.id.budgetBalance)
|
||||
|
||||
override fun onBind(item: BudgetState) {
|
||||
with(item) {
|
||||
name.text = budget.name
|
||||
if (budget.description.isNullOrBlank()) {
|
||||
description.visibility = View.GONE
|
||||
} else {
|
||||
description.visibility = View.VISIBLE
|
||||
description.text = budget.description
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_BUDGET_ID, budget.id!!)
|
||||
}
|
||||
navController.navigate(R.id.categoryListFragment, bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.wbrawner.budget.ui.budgets
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.di.ViewModelKey
|
||||
import com.wbrawner.budget.ui.base.LoadingViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Inject
|
||||
|
||||
class BudgetViewModel @Inject constructor(private val budgetRepo: BudgetRepository) : LoadingViewModel() {
|
||||
suspend fun getBudget(id: Long): Budget = showLoader {
|
||||
budgetRepo.findById(id)
|
||||
}
|
||||
|
||||
suspend fun getBudgets(): Collection<Budget> = showLoader {
|
||||
budgetRepo.findAll()
|
||||
}
|
||||
|
||||
suspend fun saveBudget(budget: Budget) = showLoader {
|
||||
if (budget.id == null)
|
||||
budgetRepo.create(budget)
|
||||
else
|
||||
budgetRepo.update(budget)
|
||||
}
|
||||
|
||||
suspend fun deleteBudgetById(id: Long) = showLoader {
|
||||
budgetRepo.delete(id)
|
||||
}
|
||||
|
||||
suspend fun getBalance(id: Long) = showLoader {
|
||||
budgetRepo.getBalance(id)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
abstract class BudgetViewModelMapper {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(BudgetViewModel::class)
|
||||
abstract fun bindViewModel(viewModel: BudgetViewModel): ViewModel
|
||||
}
|
|
@ -4,26 +4,27 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.autoDispose
|
||||
import com.wbrawner.budget.ui.fromBackgroundToMain
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import com.wbrawner.budget.ui.transactions.toLong
|
||||
import kotlinx.android.synthetic.main.activity_add_edit_category.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class AddEditCategoryActivity : AppCompatActivity() {
|
||||
private val disposables = CompositeDisposable()
|
||||
private lateinit var account: Account
|
||||
class AddEditCategoryActivity : AppCompatActivity(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
lateinit var viewModel: CategoryViewModel
|
||||
|
@ -37,40 +38,36 @@ class AddEditCategoryActivity : AppCompatActivity() {
|
|||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
(application as AllowanceApplication).appComponent.inject(this)
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory).get(CategoryViewModel::class.java)
|
||||
viewModel.getAccount(intent!!.extras!!.getLong(EXTRA_ACCOUNT_ID))
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { account, err ->
|
||||
if (err != null) {
|
||||
finish()
|
||||
// TODO: Hide a progress spinner and show error message
|
||||
return@subscribe
|
||||
}
|
||||
this@AddEditCategoryActivity.account = account
|
||||
loadCategory()
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
launch {
|
||||
val budgets = viewModel.getBudgets().toTypedArray()
|
||||
budgetSpinner.adapter = ArrayAdapter<Budget>(
|
||||
this@AddEditCategoryActivity,
|
||||
android.R.layout.simple_list_item_1,
|
||||
budgets
|
||||
)
|
||||
loadCategory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCategory() {
|
||||
if (intent?.hasExtra(EXTRA_CATEGORY_ID) == false) {
|
||||
val categoryId = intent?.extras?.getLong(EXTRA_CATEGORY_ID)
|
||||
if (categoryId == null) {
|
||||
setTitle(R.string.title_add_category)
|
||||
return
|
||||
}
|
||||
viewModel.getCategory(intent!!.extras!!.getLong(EXTRA_CATEGORY_ID))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { category, err ->
|
||||
if (err != null) {
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = false
|
||||
return@subscribe
|
||||
}
|
||||
id = category.id
|
||||
setTitle(R.string.title_edit_category)
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = true
|
||||
edit_category_name.setText(category.title)
|
||||
edit_category_amount.setText(String.format("%.02f", category.amount / 100.0f))
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
launch {
|
||||
val category = try {
|
||||
viewModel.getCategory(categoryId)
|
||||
} catch (e: Exception) {
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = false
|
||||
null
|
||||
} ?: return@launch
|
||||
id = category.id
|
||||
setTitle(R.string.title_edit_category)
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = true
|
||||
edit_category_name.setText(category.title)
|
||||
edit_category_amount.setText(String.format("%.02f", category.amount / 100.0f))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
|
@ -101,29 +98,26 @@ class AddEditCategoryActivity : AppCompatActivity() {
|
|||
}
|
||||
R.id.action_save -> {
|
||||
if (!validateFields()) return true
|
||||
viewModel.saveCategory(Category(
|
||||
id = id,
|
||||
title = edit_category_name.text.toString(),
|
||||
amount = edit_category_amount.rawValue,
|
||||
accountId = account.id!!
|
||||
))
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { _, err ->
|
||||
finish()
|
||||
}
|
||||
launch {
|
||||
viewModel.saveCategory(Category(
|
||||
id = id,
|
||||
title = edit_category_name.text.toString(),
|
||||
amount = edit_category_amount.text.toLong(),
|
||||
budgetId = (budgetSpinner.selectedItem as Budget).id!!
|
||||
))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
viewModel.deleteCategoryById(this@AddEditCategoryActivity.id!!)
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { _, _ ->
|
||||
finish()
|
||||
}
|
||||
launch {
|
||||
viewModel.deleteCategoryById(this@AddEditCategoryActivity.id!!)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
private fun validateFields(): Boolean {
|
||||
var errors = false
|
||||
if (edit_category_name.text?.isEmpty() == true) {
|
||||
|
@ -131,21 +125,11 @@ class AddEditCategoryActivity : AppCompatActivity() {
|
|||
errors = true
|
||||
}
|
||||
|
||||
if (edit_category_amount.text.isEmpty()) {
|
||||
if (edit_category_amount.text.toString().isEmpty()) {
|
||||
edit_category_amount.error = getString(R.string.required_field_amount)
|
||||
errors = true
|
||||
}
|
||||
|
||||
return !errors
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
disposables.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ACCOUNT_ID = "EXTRA_ACCOUNT_ID"
|
||||
const val EXTRA_CATEGORY_ID = "EXTRA_CATEGORY_ID"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,22 +8,21 @@ import android.view.ViewGroup
|
|||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.ui.autoDispose
|
||||
import com.wbrawner.budget.ui.categories.AddEditCategoryActivity.Companion.EXTRA_ACCOUNT_ID
|
||||
import com.wbrawner.budget.ui.categories.AddEditCategoryActivity.Companion.EXTRA_CATEGORY_ID
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class CategoryAdapter(
|
||||
private val activity: Activity,
|
||||
private val account: Account,
|
||||
private val data: List<Category>,
|
||||
private val viewModel: CategoryViewModel
|
||||
) : androidx.recyclerview.widget.RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
|
||||
) : RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_category, parent, false)
|
||||
|
@ -35,30 +34,26 @@ class CategoryAdapter(
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val category = data[position]
|
||||
holder.title?.text = category.title
|
||||
// TODO: Format according to account's currency
|
||||
// TODO: Format according to budget's currency
|
||||
holder.amount?.text = String.format("${'$'}%.02f", category.amount / 100.0f)
|
||||
holder.progress?.max = category.amount.toInt()
|
||||
viewModel.getBalance(category.id!!)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { balance, _ ->
|
||||
holder.progress?.isIndeterminate = false
|
||||
holder.progress?.setProgress(
|
||||
(-1 * balance).toInt(),
|
||||
true
|
||||
)
|
||||
holder.amount?.text = holder.itemView.context.getString(
|
||||
R.string.balance_remaning,
|
||||
(category.amount + balance) / 100.0f
|
||||
)
|
||||
}
|
||||
.autoDispose(holder.disposables)
|
||||
holder.launch {
|
||||
val balance = viewModel.getBalance(category.id!!)
|
||||
holder.progress?.isIndeterminate = false
|
||||
holder.progress?.setProgress(
|
||||
(-1 * balance).toInt(),
|
||||
true
|
||||
)
|
||||
holder.amount?.text = holder.itemView.context.getString(
|
||||
R.string.balance_remaning,
|
||||
(category.amount + balance) / 100.0f
|
||||
)
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
startActivity(
|
||||
activity,
|
||||
Intent(it.context.applicationContext, AddEditCategoryActivity::class.java)
|
||||
.apply {
|
||||
putExtra(EXTRA_ACCOUNT_ID, account.id!!)
|
||||
putExtra(EXTRA_CATEGORY_ID, category.id!!)
|
||||
},
|
||||
null
|
||||
|
@ -67,15 +62,14 @@ class CategoryAdapter(
|
|||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
holder.disposables.dispose()
|
||||
holder.disposables.clear()
|
||||
holder.coroutineContext.cancel()
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
|
||||
inner class ViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) {
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
val title: TextView? = itemView.findViewById(R.id.category_title)
|
||||
val amount: TextView? = itemView.findViewById(R.id.category_amount)
|
||||
val progress: ProgressBar? = itemView.findViewById(R.id.category_progress)
|
||||
val disposables = CompositeDisposable()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import com.wbrawner.budget.ui.base.BindableAdapter
|
||||
import com.wbrawner.budget.ui.base.BindableState
|
||||
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
|
||||
import com.wbrawner.budget.ui.transactions.TRANSACTION_VIEW
|
||||
import com.wbrawner.budget.ui.transactions.TransactionState
|
||||
import com.wbrawner.budget.ui.transactions.TransactionViewHolder
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class CategoryFragment : ListWithAddButtonFragment<CategoryViewModel>() {
|
||||
override val noItemsStringRes: Int = R.string.transactions_no_data
|
||||
override val viewModelClass: Class<CategoryViewModel> = CategoryViewModel::class.java
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>> {
|
||||
val categoryId = arguments?.getLong(EXTRA_CATEGORY_ID)
|
||||
if (categoryId == null) {
|
||||
findNavController().navigateUp()
|
||||
return Pair(emptyList(), emptyMap())
|
||||
}
|
||||
val category = viewModel.getCategory(categoryId)
|
||||
activity?.title = category.title
|
||||
// TODO: Add category details here as well
|
||||
val items = ArrayList<BindableState>()
|
||||
items.addAll(viewModel.getTransactions(categoryId).map { TransactionState(it) })
|
||||
return Pair(
|
||||
items,
|
||||
mapOf(TRANSACTION_VIEW to { v ->
|
||||
TransactionViewHolder(v, findNavController()) as BindableAdapter.BindableViewHolder<in BindableState>
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_edit) {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_CATEGORY_ID, arguments?.getLong(EXTRA_CATEGORY_ID) ?: -1)
|
||||
}
|
||||
findNavController().navigate(R.id.addEditCategoryActivity, bundle)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_editable, menu)
|
||||
}
|
||||
|
||||
override fun addItem() {
|
||||
// TODO: Open new transaction flow with budget and category pre-filled
|
||||
findNavController().navigate(R.id.addEditTransactionActivity)
|
||||
}
|
||||
}
|
|
@ -1,79 +1,97 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.emoji.widget.EmojiTextView
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.autoDispose
|
||||
import com.wbrawner.budget.ui.categories.AddEditCategoryActivity.Companion.EXTRA_ACCOUNT_ID
|
||||
import com.wbrawner.budget.ui.hideFabOnScroll
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import com.wbrawner.budget.ui.base.BindableAdapter
|
||||
import com.wbrawner.budget.ui.base.BindableState
|
||||
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CategoryListFragment : androidx.fragment.app.Fragment() {
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
lateinit var viewModel: CategoryViewModel
|
||||
lateinit var account: Account
|
||||
private val disposables = CompositeDisposable()
|
||||
var recyclerView: androidx.recyclerview.widget.RecyclerView? = null
|
||||
var noDataView: EmojiTextView? = null
|
||||
class CategoryListFragment : ListWithAddButtonFragment<CategoryViewModel>() {
|
||||
override val noItemsStringRes: Int = R.string.categories_no_data
|
||||
override val viewModelClass: Class<CategoryViewModel> = CategoryViewModel::class.java
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_transaction_list, container, false)
|
||||
recyclerView = view.findViewById(R.id.list_transactions)
|
||||
val fab = view.findViewById<FloatingActionButton>(R.id.fab_add_transaction)
|
||||
recyclerView?.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(activity)
|
||||
(activity!!.application as AllowanceApplication).appComponent.inject(this)
|
||||
viewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(CategoryViewModel::class.java)
|
||||
noDataView = view.findViewById(R.id.transaction_list_no_data)
|
||||
recyclerView?.hideFabOnScroll(fab)
|
||||
fab.setOnClickListener {
|
||||
startActivity(Intent(activity, AddEditCategoryActivity::class.java).apply {
|
||||
putExtra(EXTRA_ACCOUNT_ID, account.id)
|
||||
})
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>> {
|
||||
val budgetId = arguments?.getLong(EXTRA_BUDGET_ID)
|
||||
if (budgetId == null) {
|
||||
findNavController().navigateUp()
|
||||
return Pair(emptyList(), emptyMap())
|
||||
}
|
||||
return view
|
||||
val budget = viewModel.getBudget(budgetId)
|
||||
activity?.title = budget.name
|
||||
return Pair(
|
||||
viewModel.getCategories(budgetId).map { CategoryState(it) },
|
||||
mapOf(CATEGORY_VIEW to { v ->
|
||||
CategoryViewHolder(v, viewModel, findNavController()) as BindableAdapter.BindableViewHolder<in BindableState>
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
disposables.dispose()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.getCategories(account.id!!)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { data ->
|
||||
if (data.isEmpty()) {
|
||||
recyclerView?.adapter = null
|
||||
noDataView?.setText(R.string.categories_no_data)
|
||||
recyclerView?.visibility = View.GONE
|
||||
noDataView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
recyclerView?.adapter = CategoryAdapter(activity!!, account, data.toList(),
|
||||
viewModel)
|
||||
recyclerView?.visibility = View.VISIBLE
|
||||
noDataView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
override fun addItem() {
|
||||
startActivity(Intent(activity, AddEditCategoryActivity::class.java))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG_FRAGMENT = "categories"
|
||||
const val TITLE_FRAGMENT = R.string.title_categories
|
||||
}
|
||||
}
|
||||
|
||||
const val CATEGORY_VIEW = R.layout.list_item_category
|
||||
|
||||
class CategoryState(val category: Category) : BindableState {
|
||||
override val viewType: Int = CATEGORY_VIEW
|
||||
}
|
||||
|
||||
class CategoryViewHolder(
|
||||
itemView: View,
|
||||
private val viewModel: CategoryViewModel,
|
||||
private val navController: NavController
|
||||
) : BindableAdapter.CoroutineViewHolder<CategoryState>(itemView) {
|
||||
private val name: TextView = itemView.findViewById(R.id.category_title)
|
||||
private val amount: TextView = itemView.findViewById(R.id.category_amount)
|
||||
private val progressBar: ProgressBar = itemView.findViewById(R.id.category_progress)
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onBind(item: CategoryState) {
|
||||
with(item) {
|
||||
name.text = category.title
|
||||
// TODO: Format according to budget's currency
|
||||
amount.text = String.format("${'$'}%.02f", category.amount / 100.0f)
|
||||
progressBar.max = category.amount.toInt()
|
||||
launch {
|
||||
val balance = viewModel.getBalance(category.id!!)
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.setProgress(
|
||||
(-1 * balance).toInt(),
|
||||
true
|
||||
)
|
||||
amount.text = itemView.context.getString(
|
||||
R.string.balance_remaning,
|
||||
(category.amount + balance) / 100.0f
|
||||
)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_CATEGORY_ID, category.id ?: -1)
|
||||
}
|
||||
navController.navigate(R.id.categoryFragment, bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.di.ViewModelKey
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryListViewModel @Inject constructor(
|
||||
private val budgetRepo: BudgetRepository,
|
||||
private val categoryRepo: CategoryRepository
|
||||
) : ViewModel() {
|
||||
suspend fun getCategory(id: Long): Category = categoryRepo.findById(id)
|
||||
|
||||
suspend fun getCategories(budgetId: Long? = null): Collection<Category> =
|
||||
categoryRepo.findAll(budgetId)
|
||||
|
||||
suspend fun saveCategory(category: Category) = if (category.id == null) categoryRepo.create(category)
|
||||
else categoryRepo.update(category)
|
||||
|
||||
suspend fun deleteCategoryById(id: Long) = categoryRepo.delete(id)
|
||||
|
||||
suspend fun getBalance(id: Long) = categoryRepo.getBalance(id)
|
||||
}
|
||||
|
||||
@Module
|
||||
abstract class CategoryListViewModelMapper {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(CategoryListViewModel::class)
|
||||
abstract fun bindViewModel(viewModel: CategoryListViewModel): ViewModel
|
||||
}
|
|
@ -1,30 +1,57 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.common.account.AccountRepository
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.common.transaction.TransactionRepository
|
||||
import com.wbrawner.budget.di.ViewModelKey
|
||||
import com.wbrawner.budget.ui.base.LoadingViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryViewModel @Inject constructor(private val accountRepo: AccountRepository, private val categoryRepo:
|
||||
CategoryRepository) : ViewModel() {
|
||||
fun getAccount(id: Long) = accountRepo.findById(id)
|
||||
class CategoryViewModel @Inject constructor(
|
||||
private val transactionRepo: TransactionRepository,
|
||||
private val categoryRepo: CategoryRepository,
|
||||
private val budgetRepo: BudgetRepository
|
||||
) : LoadingViewModel() {
|
||||
suspend fun getBudget(budgetId: Long): Budget = showLoader {
|
||||
budgetRepo.findById(budgetId)
|
||||
}
|
||||
|
||||
fun getCategory(id: Long): Single<Category> = categoryRepo.findById(id)
|
||||
suspend fun getBudgets() = showLoader {
|
||||
budgetRepo.findAll()
|
||||
}
|
||||
|
||||
fun getCategories(accountId: Long): Single<Collection<Category>> = categoryRepo.findAll(accountId)
|
||||
suspend fun getTransactions(categoryId: Long) = showLoader {
|
||||
transactionRepo.findAll(categoryId = categoryId)
|
||||
}
|
||||
|
||||
fun saveCategory(category: Category) = if (category.id == null) categoryRepo.create(category)
|
||||
else categoryRepo.update(category)
|
||||
suspend fun getCategory(id: Long): Category = showLoader {
|
||||
categoryRepo.findById(id)
|
||||
}
|
||||
|
||||
fun deleteCategoryById(id: Long) = categoryRepo.delete(id)
|
||||
suspend fun getCategories(budgetId: Long? = null): Collection<Category> = showLoader {
|
||||
categoryRepo.findAll(budgetId)
|
||||
}
|
||||
|
||||
fun getBalance(id: Long) = categoryRepo.getBalance(id)
|
||||
suspend fun saveCategory(category: Category) = showLoader {
|
||||
if (category.id == null)
|
||||
categoryRepo.create(category)
|
||||
else
|
||||
categoryRepo.update(category)
|
||||
}
|
||||
|
||||
suspend fun deleteCategoryById(id: Long) = showLoader {
|
||||
categoryRepo.delete(id)
|
||||
}
|
||||
|
||||
suspend fun getBalance(id: Long) = showLoader {
|
||||
categoryRepo.getBalance(id)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
package com.wbrawner.budget.ui.overview
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.common.account.AccountRepository
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.di.ViewModelKey
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountOverviewViewModel @Inject constructor(private val accountRepo: AccountRepository) : ViewModel() {
|
||||
fun getBalance(id: Long): Single<Long> = accountRepo.getBalance(id)
|
||||
class AccountOverviewViewModel @Inject constructor(private val budgetRepo: BudgetRepository) : ViewModel() {
|
||||
suspend fun getBalance(id: Long): Long = budgetRepo.getBalance(id)
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.wbrawner.budget.ui.overview
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
|
||||
class AccountsAdapter() : RecyclerView.Adapter<AccountsAdapter.AccountViewHolder>() {
|
||||
private val accounts = mutableListOf<Budget>()
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AccountViewHolder(
|
||||
LayoutInflater.from(parent.context!!).inflate(R.layout.list_item_budget, parent, false)
|
||||
)
|
||||
|
||||
override fun getItemCount(): Int = accounts.size
|
||||
|
||||
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
|
||||
val account = accounts[holder.adapterPosition]
|
||||
holder.title.text = account.name
|
||||
holder.balance.text = account.name
|
||||
}
|
||||
|
||||
class AccountViewHolder(
|
||||
itemView: View,
|
||||
val title: TextView = itemView.findViewById(R.id.budgetName),
|
||||
val balance: TextView = itemView.findViewById(R.id.budgetBalance)
|
||||
) : RecyclerView.ViewHolder(itemView)
|
||||
}
|
|
@ -5,23 +5,25 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.autoDispose
|
||||
import com.wbrawner.budget.ui.fromBackgroundToMain
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import com.wbrawner.budget.ui.toAmountSpannable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class OverviewFragment : androidx.fragment.app.Fragment() {
|
||||
private val disposables = CompositeDisposable()
|
||||
class OverviewFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
lateinit var viewModel: AccountOverviewViewModel
|
||||
lateinit var account: Account
|
||||
lateinit var budget: Budget
|
||||
private var inflatedView: View? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -33,21 +35,14 @@ class OverviewFragment : androidx.fragment.app.Fragment() {
|
|||
return view
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.getBalance(account.id!!)
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { balance ->
|
||||
val balanceView = view!!.findViewById<TextView>(R.id.overview_current_balance)
|
||||
val color = when {
|
||||
balance > 0 -> R.color.colorTextGreen
|
||||
balance == 0L -> R.color.colorTextPrimary
|
||||
else -> R.color.colorTextRed
|
||||
}
|
||||
balanceView.setTextColor(ContextCompat.getColor(context!!, color))
|
||||
balanceView.text = String.format("${'$'}%.02f", balance / 100.0f)
|
||||
showData(inflatedView, true)
|
||||
}.autoDispose(disposables)
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
launch {
|
||||
val balance = viewModel.getBalance(budget.id!!)
|
||||
view?.findViewById<TextView>(R.id.overview_current_balance)
|
||||
?.text = balance.toAmountSpannable(view?.context)
|
||||
showData(inflatedView, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showData(view: View?, show: Boolean) {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package com.wbrawner.budget.ui.profile
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.wbrawner.budget.R
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class ProfileFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_profile, container, false)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,9 +1,15 @@
|
|||
package com.wbrawner.budget.ui.transactions
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.format.DateFormat
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NavUtils
|
||||
|
@ -11,36 +17,31 @@ import androidx.core.app.TaskStackBuilder
|
|||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.transaction.Transaction
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.EXTRA_TRANSACTION_ID
|
||||
import com.wbrawner.budget.ui.MainActivity
|
||||
import com.wbrawner.budget.ui.autoDispose
|
||||
import com.wbrawner.budget.ui.fromBackgroundToMain
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.activity_add_edit_transaction.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
const val EXTRA_ACCOUNT_ID = "EXTRA_ACCOUNT_ID"
|
||||
const val EXTRA_TRANSACTION_ID = "EXTRA_TRANSACTION_ID"
|
||||
|
||||
class AddEditTransactionActivity : AppCompatActivity() {
|
||||
private val disposables = CompositeDisposable()
|
||||
private lateinit var account: Account
|
||||
class AddEditTransactionActivity : AppCompatActivity(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
private lateinit var viewModel: AddEditTransactionViewModel
|
||||
var id: Long? = null
|
||||
var menu: Menu? = null
|
||||
var transaction: Transaction? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent?.hasExtra(EXTRA_ACCOUNT_ID) != true) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
setContentView(R.layout.activity_add_edit_transaction)
|
||||
setSupportActionBar(action_bar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
@ -48,78 +49,112 @@ class AddEditTransactionActivity : AppCompatActivity() {
|
|||
edit_transaction_type_expense.isChecked = true
|
||||
(application as AllowanceApplication).appComponent.inject(this)
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory).get(AddEditTransactionViewModel::class.java)
|
||||
viewModel.getAccount(intent!!.extras!!.getLong(EXTRA_ACCOUNT_ID))
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { account, err ->
|
||||
if (err != null) {
|
||||
finish()
|
||||
// TODO: Hide a progress spinner and show error message
|
||||
return@subscribe
|
||||
}
|
||||
this@AddEditTransactionActivity.account = account
|
||||
loadCategories()
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
}
|
||||
launch {
|
||||
val accounts = viewModel.getAccounts().toTypedArray()
|
||||
setCategories()
|
||||
budgetSpinner.adapter = ArrayAdapter<Budget>(
|
||||
this@AddEditTransactionActivity,
|
||||
android.R.layout.simple_list_item_1,
|
||||
accounts
|
||||
)
|
||||
|
||||
private fun loadCategories() {
|
||||
viewModel.getCategories(account.id!!)
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { categories ->
|
||||
val adapter = ArrayAdapter<Category>(
|
||||
this@AddEditTransactionActivity,
|
||||
android.R.layout.simple_list_item_1
|
||||
)
|
||||
adapter.add(Category(id = 0, title = getString(R.string.uncategorized),
|
||||
amount = 0, accountId = 0))
|
||||
// TODO: Add option to create new category on-the-fly
|
||||
if (!categories.isEmpty()) {
|
||||
adapter.addAll(categories)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
edit_transaction_category.adapter = adapter
|
||||
}
|
||||
if (intent?.hasExtra(EXTRA_TRANSACTION_ID) == true) {
|
||||
loadTransaction()
|
||||
budgetSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
this@AddEditTransactionActivity.launch {
|
||||
val account = budgetSpinner.selectedItem as Budget
|
||||
setCategories(viewModel.getCategories(account.id!!))
|
||||
}
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
|
||||
}
|
||||
|
||||
private fun loadTransaction() {
|
||||
viewModel.getTransaction(intent!!.extras!!.getLong(EXTRA_TRANSACTION_ID))
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { transaction, err ->
|
||||
if (err != null) {
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = false
|
||||
return@subscribe
|
||||
}
|
||||
id = transaction.id
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = true
|
||||
edit_transaction_title.setText(transaction.title)
|
||||
edit_transaction_description.setText(transaction.description)
|
||||
edit_transaction_amount.setText(String.format("%.02f", transaction.amount / 100.0f))
|
||||
if (transaction.expense) {
|
||||
edit_transaction_type_expense.isChecked = true
|
||||
} else {
|
||||
edit_transaction_type_income.isChecked = true
|
||||
}
|
||||
val field = Calendar.getInstance()
|
||||
field.time = transaction.date
|
||||
val year = field.get(Calendar.YEAR)
|
||||
val month = field.get(Calendar.MONTH)
|
||||
val day = field.get(Calendar.DAY_OF_MONTH)
|
||||
edit_transaction_date.updateDate(year, month, day)
|
||||
transaction.categoryId?.let {
|
||||
for (i in 0 until edit_transaction_category.adapter.count) {
|
||||
if (it == (edit_transaction_category.adapter.getItem(i) as Category).id) {
|
||||
edit_transaction_category.setSelection(i)
|
||||
break
|
||||
}
|
||||
loadTransaction()
|
||||
transactionDate.setOnClickListener {
|
||||
val currentDate = DateFormat.getDateFormat(this@AddEditTransactionActivity)
|
||||
.parse(transactionDate.text.toString())
|
||||
DatePickerDialog(
|
||||
this@AddEditTransactionActivity,
|
||||
{ _, year, month, dayOfMonth ->
|
||||
transactionDate.text = DateFormat.getDateFormat(this@AddEditTransactionActivity)
|
||||
.format(Date(year, month, dayOfMonth))
|
||||
},
|
||||
currentDate.year + 1900,
|
||||
currentDate.month,
|
||||
currentDate.date
|
||||
).show()
|
||||
}
|
||||
transactionTime.setOnClickListener {
|
||||
val currentDate = DateFormat.getTimeFormat(this@AddEditTransactionActivity)
|
||||
.parse(transactionTime.text.toString())
|
||||
TimePickerDialog(
|
||||
this@AddEditTransactionActivity,
|
||||
TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
|
||||
val newTime = Date().apply {
|
||||
hours = hourOfDay
|
||||
minutes = minute
|
||||
}
|
||||
}
|
||||
}
|
||||
transactionTime.text = DateFormat.getTimeFormat(this@AddEditTransactionActivity)
|
||||
.format(newTime)
|
||||
},
|
||||
currentDate.hours,
|
||||
currentDate.minutes,
|
||||
DateFormat.is24HourFormat(this@AddEditTransactionActivity)
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadTransaction() {
|
||||
transaction = try {
|
||||
viewModel.getTransaction(intent!!.extras!!.getLong(EXTRA_TRANSACTION_ID))
|
||||
} catch (e: Exception) {
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = false
|
||||
val date = Date()
|
||||
transactionDate.text = DateFormat.getDateFormat(this).format(date)
|
||||
transactionTime.text = DateFormat.getTimeFormat(this).format(date)
|
||||
return
|
||||
}
|
||||
setTitle(R.string.title_edit_transaction)
|
||||
id = transaction?.id
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = true
|
||||
edit_transaction_title.setText(transaction?.title)
|
||||
edit_transaction_description.setText(transaction?.description)
|
||||
edit_transaction_amount.setText(String.format("%.02f", transaction!!.amount / 100.0f))
|
||||
if (transaction!!.expense) {
|
||||
edit_transaction_type_expense.isChecked = true
|
||||
} else {
|
||||
edit_transaction_type_income.isChecked = true
|
||||
}
|
||||
transactionDate.text = DateFormat.getDateFormat(this).format(transaction!!.date)
|
||||
transactionTime.text = DateFormat.getTimeFormat(this).format(transaction!!.date)
|
||||
transaction?.categoryId?.let {
|
||||
for (i in 0 until edit_transaction_category.adapter.count) {
|
||||
if (it == (edit_transaction_category.adapter.getItem(i) as Category).id) {
|
||||
edit_transaction_category.setSelection(i)
|
||||
break
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCategories(categories: Collection<Category> = emptyList()) {
|
||||
val adapter = ArrayAdapter<Category>(
|
||||
this@AddEditTransactionActivity,
|
||||
android.R.layout.simple_list_item_1
|
||||
)
|
||||
adapter.add(Category(id = 0, title = getString(R.string.uncategorized),
|
||||
amount = 0, budgetId = 0))
|
||||
adapter.addAll(categories)
|
||||
edit_transaction_category.adapter = adapter
|
||||
transaction?.categoryId?.let {
|
||||
for (i in 0 until edit_transaction_category.adapter.count) {
|
||||
if (it == (edit_transaction_category.adapter.getItem(i) as Category).id) {
|
||||
edit_transaction_category.setSelection(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
|
@ -135,38 +170,35 @@ class AddEditTransactionActivity : AppCompatActivity() {
|
|||
when (item?.itemId) {
|
||||
android.R.id.home -> onNavigateUp()
|
||||
R.id.action_save -> {
|
||||
val field = edit_transaction_date
|
||||
val cal = Calendar.getInstance()
|
||||
cal.set(field.year, field.month, field.dayOfMonth)
|
||||
var categoryId = (edit_transaction_category.selectedItem as? Category)?.id
|
||||
val date = DateFormat.getDateFormat(this).parse(transactionDate.text.toString())
|
||||
val time = DateFormat.getTimeFormat(this).parse(transactionTime.text.toString())
|
||||
date.hours = time.hours
|
||||
date.minutes = time.minutes
|
||||
val categoryId = (edit_transaction_category.selectedItem as? Category)?.id
|
||||
?.let {
|
||||
if (it > 0) it
|
||||
else null
|
||||
}
|
||||
viewModel.saveTransaction(Transaction(
|
||||
id = id,
|
||||
accountId = account.id!!,
|
||||
title = edit_transaction_title.text.toString(),
|
||||
date = cal.time,
|
||||
description = edit_transaction_description.text.toString(),
|
||||
amount = edit_transaction_amount.rawValue,
|
||||
expense = edit_transaction_type_expense.isChecked,
|
||||
categoryId = categoryId,
|
||||
createdBy = (application as AllowanceApplication).currentUser!!.id!!
|
||||
))
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { transaction, err ->
|
||||
onNavigateUp()
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
launch {
|
||||
viewModel.saveTransaction(Transaction(
|
||||
id = id,
|
||||
budgetId = (budgetSpinner.selectedItem as Budget).id!!,
|
||||
title = edit_transaction_title.text.toString(),
|
||||
date = date,
|
||||
description = edit_transaction_description.text.toString(),
|
||||
amount = edit_transaction_amount.text.toLong(),
|
||||
expense = edit_transaction_type_expense.isChecked,
|
||||
categoryId = categoryId,
|
||||
createdBy = (application as AllowanceApplication).currentUser!!.id!!
|
||||
))
|
||||
onNavigateUp()
|
||||
}
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
viewModel.deleteTransaction(this@AddEditTransactionActivity.id!!)
|
||||
.fromBackgroundToMain()
|
||||
.subscribe { _, err ->
|
||||
err?.printStackTrace()
|
||||
onNavigateUp()
|
||||
}
|
||||
launch {
|
||||
viewModel.deleteTransaction(this@AddEditTransactionActivity.id!!)
|
||||
onNavigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
@ -190,9 +222,6 @@ class AddEditTransactionActivity : AppCompatActivity() {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
disposables.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
fun Editable?.toLong(): Long = toString().toDouble().toLong() * 100
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.wbrawner.budget.ui.transactions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.common.account.AccountRepository
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.common.transaction.Transaction
|
||||
import com.wbrawner.budget.common.transaction.TransactionRepository
|
||||
|
@ -12,22 +12,22 @@ import dagger.multibindings.IntoMap
|
|||
import javax.inject.Inject
|
||||
|
||||
class AddEditTransactionViewModel @Inject constructor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val budgetRepository: BudgetRepository,
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val transactionRepository: TransactionRepository
|
||||
) : ViewModel() {
|
||||
fun getCategories(accountId: Long) = categoryRepository.findAll(accountId)
|
||||
suspend fun getCategories(accountId: Long) = categoryRepository.findAll(accountId)
|
||||
|
||||
fun getTransaction(id: Long) = transactionRepository.findById(id)
|
||||
suspend fun getTransaction(id: Long) = transactionRepository.findById(id)
|
||||
|
||||
fun saveTransaction(transaction: Transaction) = if (transaction.id == null)
|
||||
suspend fun saveTransaction(transaction: Transaction) = if (transaction.id == null)
|
||||
transactionRepository.create(transaction)
|
||||
else
|
||||
transactionRepository.update(transaction)
|
||||
|
||||
fun deleteTransaction(id: Long) = transactionRepository.delete(id)
|
||||
suspend fun deleteTransaction(id: Long) = transactionRepository.delete(id)
|
||||
|
||||
fun getAccount(id: Long) = accountRepository.findById(id)
|
||||
suspend fun getAccounts() = budgetRepository.findAll()
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
package com.wbrawner.budget.ui.transactions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.transaction.Transaction
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class TransactionAdapter(private val activity: Activity, private val data: List<Transaction>)
|
||||
: androidx.recyclerview.widget.RecyclerView.Adapter<TransactionAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_transaction, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val transaction: Transaction = data[position]
|
||||
holder.title?.text = transaction.title
|
||||
holder.date?.text = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT).format(transaction.date)
|
||||
holder.amount?.text = String.format("${'$'}%.02f", transaction.amount / 100.0f)
|
||||
val context = holder.itemView.context
|
||||
val color = if (transaction.expense) R.color.colorTextRed else R.color.colorTextGreen
|
||||
holder.amount?.setTextColor(ContextCompat.getColor(context, color))
|
||||
holder.itemView.setOnClickListener {
|
||||
startActivity(
|
||||
activity,
|
||||
Intent(it.context.applicationContext, AddEditTransactionActivity::class.java)
|
||||
.apply {
|
||||
putExtra(EXTRA_ACCOUNT_ID, transaction.accountId)
|
||||
putExtra(EXTRA_TRANSACTION_ID, transaction.id)
|
||||
},
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) {
|
||||
val title: TextView? = itemView.findViewById(R.id.transaction_title)
|
||||
val date: TextView? = itemView.findViewById(R.id.transaction_date)
|
||||
val amount: TextView? = itemView.findViewById(R.id.transaction_amount)
|
||||
}
|
||||
}
|
|
@ -1,83 +1,84 @@
|
|||
package com.wbrawner.budget.ui.transactions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.emoji.widget.EmojiTextView
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import com.wbrawner.budget.common.transaction.Transaction
|
||||
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import com.wbrawner.budget.ui.EXTRA_TRANSACTION_ID
|
||||
import com.wbrawner.budget.ui.base.BindableAdapter
|
||||
import com.wbrawner.budget.ui.base.BindableState
|
||||
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class TransactionListFragment : androidx.fragment.app.Fragment() {
|
||||
private val disposables = CompositeDisposable()
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
lateinit var listViewModel: TransactionListViewModel
|
||||
lateinit var account: Account
|
||||
lateinit var recyclerView: androidx.recyclerview.widget.RecyclerView
|
||||
lateinit var noDataView: EmojiTextView
|
||||
class TransactionListFragment : ListWithAddButtonFragment<TransactionListViewModel>() {
|
||||
override val viewModelClass: Class<TransactionListViewModel> = TransactionListViewModel::class.java
|
||||
override val noItemsStringRes: Int = R.string.transactions_no_data
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_transaction_list, container, false)
|
||||
recyclerView = view.findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.list_transactions)
|
||||
noDataView = view.findViewById<EmojiTextView>(R.id.transaction_list_no_data)
|
||||
val fab = view.findViewById<FloatingActionButton>(R.id.fab_add_transaction)
|
||||
recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(activity)
|
||||
(activity!!.application as AllowanceApplication).appComponent.inject(this)
|
||||
listViewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(TransactionListViewModel::class.java)
|
||||
recyclerView.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: androidx.recyclerview.widget.RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0) fab.hide() else fab.show()
|
||||
}
|
||||
})
|
||||
fab.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(activity, AddEditTransactionActivity::class.java).apply {
|
||||
putExtra(EXTRA_ACCOUNT_ID, account.id!!)
|
||||
}
|
||||
)
|
||||
}
|
||||
return view
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
listViewModel.getTransactions(account.id!!)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { data ->
|
||||
if (data.isEmpty()) {
|
||||
recyclerView.adapter = null
|
||||
noDataView.setText(R.string.transactions_no_data)
|
||||
recyclerView.visibility = View.GONE
|
||||
noDataView.visibility = View.VISIBLE
|
||||
} else {
|
||||
recyclerView.adapter = TransactionAdapter(activity!!, data.toList())
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
noDataView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
.autoDispose(disposables)
|
||||
override fun addItem() {
|
||||
startActivity(Intent(activity, AddEditTransactionActivity::class.java))
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
disposables.dispose()
|
||||
super.onDestroyView()
|
||||
override suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>> {
|
||||
return Pair(
|
||||
viewModel.getTransactions(
|
||||
arguments?.getLong(EXTRA_BUDGET_ID),
|
||||
arguments?.getLong(EXTRA_CATEGORY_ID)
|
||||
).map { TransactionState(it) },
|
||||
mapOf(TRANSACTION_VIEW to { v ->
|
||||
TransactionViewHolder(v, findNavController()) as BindableAdapter.BindableViewHolder<in BindableState>
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG_FRAGMENT = "transactions"
|
||||
const val TITLE_FRAGMENT = R.string.title_transactions
|
||||
}
|
||||
}
|
||||
|
||||
const val TRANSACTION_VIEW = R.layout.list_item_transaction
|
||||
|
||||
class TransactionState(val transaction: Transaction) : BindableState {
|
||||
override val viewType: Int = TRANSACTION_VIEW
|
||||
}
|
||||
|
||||
class TransactionViewHolder(
|
||||
itemView: View,
|
||||
private val navController: NavController
|
||||
) : BindableAdapter.CoroutineViewHolder<TransactionState>(itemView) {
|
||||
private val name: TextView = itemView.findViewById(R.id.transaction_title)
|
||||
private val date: TextView = itemView.findViewById(R.id.transaction_date)
|
||||
private val amount: TextView = itemView.findViewById(R.id.transaction_amount)
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onBind(item: TransactionState) {
|
||||
with(item) {
|
||||
name.text = transaction.title
|
||||
date.text = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT).format(transaction.date)
|
||||
amount.text = String.format("${'$'}%.02f", transaction.amount / 100.0f)
|
||||
val context = itemView.context
|
||||
val color = if (transaction.expense) R.color.colorTextRed else R.color.colorTextGreen
|
||||
amount.setTextColor(ContextCompat.getColor(context, color))
|
||||
itemView.setOnClickListener {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_BUDGET_ID, transaction.budgetId)
|
||||
putLong(EXTRA_TRANSACTION_ID, transaction.id ?: -1)
|
||||
}
|
||||
navController.navigate(R.id.addEditTransactionActivity, bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,19 @@
|
|||
package com.wbrawner.budget.ui.transactions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.common.transaction.Transaction
|
||||
import com.wbrawner.budget.common.transaction.TransactionRepository
|
||||
import com.wbrawner.budget.di.ViewModelKey
|
||||
import com.wbrawner.budget.ui.base.LoadingViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class TransactionListViewModel @Inject constructor(private val transactionRepo: TransactionRepository) :
|
||||
ViewModel
|
||||
() {
|
||||
fun getTransactions(accountId: Long): Single<Collection<Transaction>> =
|
||||
transactionRepo.findAll(accountId)
|
||||
LoadingViewModel() {
|
||||
suspend fun getTransactions(budgetId: Long? = null, categoryId: Long? = null) = showLoader {
|
||||
transactionRepo.findAll(budgetId = budgetId, categoryId = categoryId)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4,10v7h3v-7L4,10zM10,10v7h3v-7h-3zM2,22h19v-3L2,19v3zM16,10v7h3v-7h-3zM11.5,1L2,6v2h19L21,6l-9.5,-5z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_edit_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_edit_black_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_person_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_person_black_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
|
||||
</vector>
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -32,7 +31,6 @@
|
|||
android:id="@+id/container_edit_category_name"
|
||||
style="@style/AppTheme.EditText.Container"
|
||||
android:hint="@string/prompt_category_name"
|
||||
app:layout_constraintBottom_toTopOf="@+id/container_edit_category_amount"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
@ -47,38 +45,30 @@
|
|||
android:id="@+id/container_edit_category_amount"
|
||||
style="@style/AppTheme.EditText.Container"
|
||||
android:hint="@string/prompt_category_amount"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/container_edit_category_name">
|
||||
|
||||
<com.blackcat.currencyedittext.CurrencyEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_category_amount"
|
||||
style="@style/AppTheme.EditText"
|
||||
android:text="0.00"
|
||||
android:inputType="number"
|
||||
tools:ignore="HardcodedText" />
|
||||
android:inputType="numberDecimal" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!--<TextView-->
|
||||
<!--android:id="@+id/container_edit_transaction_date"-->
|
||||
<!--style="@style/AppTheme.EditText.Hint"-->
|
||||
<!--android:layout_width="match_parent"-->
|
||||
<!--android:layout_height="wrap_content"-->
|
||||
<!--android:layout_marginStart="4dp"-->
|
||||
<!--android:text="@string/prompt_transaction_date"-->
|
||||
<!--app:layout_constraintBottom_toTopOf="@+id/edit_transaction_date"-->
|
||||
<!--app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!--app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!--app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_amount" />-->
|
||||
<TextView
|
||||
android:id="@+id/budgetHint"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/prompt_account"
|
||||
app:layout_constraintTop_toBottomOf="@+id/container_edit_category_amount"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!--<DatePicker-->
|
||||
<!--android:id="@+id/edit_transaction_date"-->
|
||||
<!--style="@style/AppTheme.DatePicker"-->
|
||||
<!--app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!--app:layout_constraintEnd_toEndOf="parent"-->
|
||||
<!--app:layout_constraintStart_toStartOf="parent"-->
|
||||
<!--app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_date" />-->
|
||||
<androidx.appcompat.widget.AppCompatSpinner
|
||||
android:id="@+id/budgetSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/budgetHint" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/action_bar"
|
||||
|
@ -64,10 +65,11 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_description">
|
||||
|
||||
<com.blackcat.currencyedittext.CurrencyEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_transaction_amount"
|
||||
style="@style/AppTheme.EditText"
|
||||
android:inputType="numberDecimal" />
|
||||
android:inputType="numberDecimal"
|
||||
android:scrollHorizontally="false" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<RadioGroup
|
||||
|
@ -81,6 +83,7 @@
|
|||
<RadioButton
|
||||
android:id="@+id/edit_transaction_type_expense"
|
||||
android:text="@string/type_expense"
|
||||
android:checked="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
<RadioButton
|
||||
|
@ -101,13 +104,49 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_type" />
|
||||
|
||||
<DatePicker
|
||||
android:id="@+id/edit_transaction_date"
|
||||
style="@style/AppTheme.DatePicker"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
<TextView
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/transactionDate"
|
||||
tools:text="9/23/2019"
|
||||
android:padding="8dp"
|
||||
android:textColor="@color/colorTextPrimary"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintEnd_toStartOf="@+id/transactionTime"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_date" />
|
||||
|
||||
<TextView
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/transactionTime"
|
||||
android:padding="8dp"
|
||||
android:textColor="@color/colorTextPrimary"
|
||||
tools:text="10:23 pm"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/transactionDate"
|
||||
app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_date" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/budgetHint"
|
||||
style="@style/AppTheme.EditText.Hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/prompt_budget"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transactionDate" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/budgetSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/budgetHint" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/container_edit_transaction_category"
|
||||
style="@style/AppTheme.EditText.Hint"
|
||||
|
@ -117,7 +156,7 @@
|
|||
android:text="@string/prompt_transaction_category"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/edit_transaction_date" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/budgetSpinner" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/edit_transaction_category"
|
||||
|
|
|
@ -1,107 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/auth_content"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.SplashActivity">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/loginContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_rounded"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fitsSystemWindows="true"
|
||||
android:visibility="gone"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/formPrompt"
|
||||
android:text="Login to manage your budget"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/usernameContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:hint="@string/prompt_username"
|
||||
app:boxCornerRadiusBottomEnd="16dp"
|
||||
app:boxCornerRadiusBottomStart="16dp"
|
||||
app:boxCornerRadiusTopEnd="16dp"
|
||||
app:boxCornerRadiusTopStart="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/formPrompt">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:hint="@string/prompt_password"
|
||||
app:boxCornerRadiusBottomEnd="16dp"
|
||||
app:boxCornerRadiusBottomStart="16dp"
|
||||
app:boxCornerRadiusTopEnd="16dp"
|
||||
app:boxCornerRadiusTopStart="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/usernameContainer">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/submit"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:backgroundTint="@color/colorAccent"
|
||||
android:text="@string/action_submit"
|
||||
android:textColor="@color/colorTextPrimaryInverted"
|
||||
app:cornerRadius="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/passwordContainer" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/auth_graph"
|
||||
tools:context=".ui.SplashActivity" />
|
||||
|
|
|
@ -8,21 +8,12 @@
|
|||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/action_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/content_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/action_bar"
|
||||
app:layout_constraintBottom_toTopOf="@+id/menu_main"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/menu_main"
|
||||
|
@ -33,4 +24,16 @@
|
|||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:menu="@menu/main_navigation" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/content_container"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/menu_main"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/action_bar"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
14
app/src/main/res/layout/fragment_accounts.xml
Normal file
14
app/src/main/res/layout/fragment_accounts.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.budgets.BudgetListFragment">
|
||||
|
||||
<!-- TODO: Update blank fragment layout -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/hello_blank_fragment" />
|
||||
|
||||
</FrameLayout>
|
57
app/src/main/res/layout/fragment_add_edit_budget.xml
Normal file
57
app/src/main/res/layout/fragment_add_edit_budget.xml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
tools:context=".ui.budgets.AddEditBudgetFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/descriptionContainer"
|
||||
style="@style/AppTheme.EditText.Container"
|
||||
android:hint="@string/prompt_account_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nameContainer">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/description"
|
||||
style="@style/AppTheme.EditText"
|
||||
android:inputType="textMultiLine|textCapSentences" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nameContainer"
|
||||
style="@style/AppTheme.EditText.Container"
|
||||
android:hint="@string/prompt_account_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/name"
|
||||
style="@style/AppTheme.EditText"
|
||||
android:inputType="text|textCapWords" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/usersSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_share_with"
|
||||
app:layout_constraintTop_toBottomOf="@+id/descriptionContainer" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/usersContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/usersSearch" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
41
app/src/main/res/layout/fragment_list_with_add_button.xml
Normal file
41
app/src/main/res/layout/fragment_list_with_add_button.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/noItemsTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/colorTextPrimary"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_vertical|center_horizontal" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/addFab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:srcCompat="@drawable/ic_add_white_24dp" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
129
app/src/main/res/layout/fragment_login.xml
Normal file
129
app/src/main/res/layout/fragment_login.xml
Normal file
|
@ -0,0 +1,129 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/loginContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@drawable/bg_rounded"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/formPrompt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/info_login"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/submit"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:backgroundTint="@color/colorAccent"
|
||||
android:text="@string/action_login"
|
||||
android:textColor="@color/colorTextPrimaryInverted"
|
||||
app:cornerRadius="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/passwordContainer" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/forgotPasswordLink"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/title_forgot_password"
|
||||
app:cornerRadius="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/submit" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/registerButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/title_register"
|
||||
app:cornerRadius="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/forgotPasswordLink" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:hint="@string/prompt_password"
|
||||
app:boxCornerRadiusBottomEnd="16dp"
|
||||
app:boxCornerRadiusBottomStart="16dp"
|
||||
app:boxCornerRadiusTopEnd="16dp"
|
||||
app:boxCornerRadiusTopStart="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/usernameContainer">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:nextFocusLeft="@+id/username"
|
||||
android:nextFocusUp="@+id/username" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/usernameContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:hint="@string/prompt_username"
|
||||
app:boxCornerRadiusBottomEnd="16dp"
|
||||
app:boxCornerRadiusBottomStart="16dp"
|
||||
app:boxCornerRadiusTopEnd="16dp"
|
||||
app:boxCornerRadiusTopStart="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/formPrompt">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:nextFocusRight="@+id/password"
|
||||
android:nextFocusDown="@+id/password"
|
||||
android:nextFocusForward="@+id/password" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -22,11 +22,20 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:textSize="36sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/accountsList"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/overview_current_balance_label" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/accountsList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/overview_current_balance" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/overview_no_data"
|
||||
android:layout_width="0dp"
|
||||
|
|
14
app/src/main/res/layout/fragment_profile.xml
Normal file
14
app/src/main/res/layout/fragment_profile.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.profile.ProfileFragment">
|
||||
|
||||
<!-- TODO: Update blank fragment layout -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/hello_blank_fragment" />
|
||||
|
||||
</FrameLayout>
|
14
app/src/main/res/layout/fragment_register.xml
Normal file
14
app/src/main/res/layout/fragment_register.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.auth.LoginFragment">
|
||||
|
||||
<!-- TODO: Update blank fragment layout -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/hello_blank_fragment" />
|
||||
|
||||
</FrameLayout>
|
49
app/src/main/res/layout/list_item_budget.xml
Normal file
49
app/src/main/res/layout/list_item_budget.xml
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeight"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/budgetName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_constraintBottom_toTopOf="@+id/budgetDescription"
|
||||
app:layout_constraintEnd_toStartOf="@+id/budgetBalance"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="US Budget" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/budgetDescription"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/budgetBalance"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/budgetName"
|
||||
tools:text="Monthly expenses in the US" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/budgetBalance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="$1,000" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
15
app/src/main/res/layout/list_item_user.xml
Normal file
15
app/src/main/res/layout/list_item_user.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
|
@ -1,15 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_overview"
|
||||
android:icon="@drawable/ic_baseline_dashboard_24dp"
|
||||
android:title="@string/title_dashboard" />
|
||||
<item
|
||||
android:id="@+id/action_transactions"
|
||||
android:id="@+id/transactionListFragment"
|
||||
android:icon="@drawable/ic_attach_money_black_24dp"
|
||||
android:title="@string/title_transactions" />
|
||||
<item
|
||||
android:id="@+id/action_categories"
|
||||
android:id="@+id/budgetsFragment"
|
||||
android:icon="@drawable/ic_baseline_category_24px"
|
||||
android:title="@string/title_categories" />
|
||||
android:title="@string/title_budgets" />
|
||||
<item
|
||||
android:id="@+id/profileFragment"
|
||||
android:icon="@drawable/ic_person_black_24dp"
|
||||
android:title="@string/title_profile" />
|
||||
</menu>
|
||||
|
|
9
app/src/main/res/menu/menu_editable.xml
Normal file
9
app/src/main/res/menu/menu_editable.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_edit"
|
||||
android:icon="@drawable/ic_edit_black_24dp"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/title_edit" />
|
||||
</menu>
|
32
app/src/main/res/navigation/auth_graph.xml
Normal file
32
app/src/main/res/navigation/auth_graph.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/placeholder">
|
||||
<fragment
|
||||
android:id="@+id/loginFragment"
|
||||
android:name="com.wbrawner.budget.ui.auth.LoginFragment"
|
||||
android:label="LoginFragment">
|
||||
<argument
|
||||
android:name="username"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="password"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/registerFragment"
|
||||
android:name="com.wbrawner.budget.ui.auth.RegisterFragment"
|
||||
android:label="RegisterFragment" />
|
||||
<fragment
|
||||
android:id="@+id/placeholder"
|
||||
android:name="androidx.fragment.app.Fragment" />
|
||||
<activity
|
||||
android:id="@+id/mainActivity"
|
||||
android:name="com.wbrawner.budget.ui.MainActivity"
|
||||
android:label="activity_transaction_list"
|
||||
tools:layout="@layout/activity_transaction_list" />
|
||||
</navigation>
|
56
app/src/main/res/navigation/nav_graph.xml
Normal file
56
app/src/main/res/navigation/nav_graph.xml
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/categoryListFragment">
|
||||
<fragment
|
||||
android:id="@+id/categoryListFragment"
|
||||
android:name="com.wbrawner.budget.ui.categories.CategoryListFragment"
|
||||
android:label="Categories"
|
||||
tools:layout="@layout/fragment_transaction_list">
|
||||
<action
|
||||
android:id="@+id/action_categoryListFragment_to_categoryFragment"
|
||||
app:destination="@id/categoryFragment" />
|
||||
<action
|
||||
android:id="@+id/action_categoryListFragment_to_addEditBudget"
|
||||
app:destination="@id/addEditBudget" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/transactionListFragment"
|
||||
android:name="com.wbrawner.budget.ui.transactions.TransactionListFragment"
|
||||
android:label="@string/title_transactions"
|
||||
tools:layout="@layout/fragment_transaction_list" />
|
||||
<fragment
|
||||
android:id="@+id/budgetsFragment"
|
||||
android:name="com.wbrawner.budget.ui.budgets.BudgetListFragment"
|
||||
android:label="@string/title_budgets"
|
||||
tools:layout="@layout/fragment_list_with_add_button">
|
||||
<action
|
||||
android:id="@+id/action_budgetsFragment_to_categoryListFragment"
|
||||
app:destination="@id/categoryListFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/addEditBudget"
|
||||
android:name="com.wbrawner.budget.ui.budgets.AddEditBudgetFragment"
|
||||
android:label="@string/title_add_budget"
|
||||
tools:layout="@layout/fragment_add_edit_budget" />
|
||||
<fragment
|
||||
android:id="@+id/profileFragment"
|
||||
android:name="com.wbrawner.budget.ui.profile.ProfileFragment"
|
||||
android:label="@string/title_profile"
|
||||
tools:layout="@layout/fragment_profile" />
|
||||
<fragment
|
||||
android:id="@+id/categoryFragment"
|
||||
android:name="com.wbrawner.budget.ui.categories.CategoryFragment"
|
||||
android:label="fragment_category"
|
||||
tools:layout="@layout/fragment_list_with_add_button" />
|
||||
<activity
|
||||
android:id="@+id/addEditTransactionActivity"
|
||||
android:name="com.wbrawner.budget.ui.transactions.AddEditTransactionActivity"
|
||||
android:label="AddEditTransactionActivity" />
|
||||
<activity
|
||||
android:id="@+id/addEditCategoryActivity"
|
||||
android:name="com.wbrawner.budget.ui.categories.AddEditCategoryActivity"
|
||||
android:label="@string/title_add_category" />
|
||||
</navigation>
|
|
@ -31,4 +31,22 @@
|
|||
<string name="prompt_password">Password</string>
|
||||
<string name="action_submit">Submit</string>
|
||||
<string name="error_required_field">This is a required field</string>
|
||||
|
||||
<!-- TODO: Remove or change this placeholder text -->
|
||||
<string name="hello_blank_fragment">Hello blank fragment</string>
|
||||
<string name="title_accounts">Accounts</string>
|
||||
<string name="title_profile">Profile</string>
|
||||
<string name="info_login">Login to manage your budget</string>
|
||||
<string name="action_login">Login</string>
|
||||
<string name="title_register">Register</string>
|
||||
<string name="title_forgot_password">Forgot password?</string>
|
||||
<string name="prompt_account">Account</string>
|
||||
<string name="prompt_account_name">Name</string>
|
||||
<string name="prompt_account_description">Description</string>
|
||||
<string name="prompt_share_with">Share with...</string>
|
||||
<string name="title_budgets">Budgets</string>
|
||||
<string name="prompt_budget">Budget</string>
|
||||
<string name="title_add_budget">Add Budget</string>
|
||||
<string name="title_edit_budget">Edit Budget</string>
|
||||
<string name="title_edit">Edit</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="BaseTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<style name="BaseTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
|
@ -10,11 +10,17 @@
|
|||
<item name="android:statusBarColor">?android:attr/windowBackground</item>
|
||||
<item name="android:windowBackground">@color/colorBackgroundPrimary</item>
|
||||
<item name="android:navigationBarColor">@color/colorBackgroundPrimary</item>
|
||||
<item name="android:timePickerDialogTheme">
|
||||
@style/Theme.MaterialComponents.DayNight.Dialog.Alert
|
||||
</item>
|
||||
<item name="android:datePickerDialogTheme">
|
||||
@style/Theme.MaterialComponents.DayNight.Dialog.Alert
|
||||
</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="BaseTheme" />
|
||||
|
||||
<style name="SplashTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<style name="SplashTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/bg_splash</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
|
@ -25,24 +31,15 @@
|
|||
<item name="android:layout_width">match_parent</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.EditText.Container" parent="AppTheme.EditText">
|
||||
<style name="AppTheme.EditText.Container" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.EditText.Hint" parent="TextAppearance.Design.Hint">
|
||||
</style>
|
||||
|
||||
<style name="BaseTheme.DatePicker">
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:startYear">2000</item>
|
||||
<item name="android:calendarViewShown">false</item>
|
||||
<item name="android:datePickerMode">spinner</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.DatePicker" parent="BaseTheme.DatePicker">
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.AlertDialog" parent="Theme.AppCompat.DayNight.Dialog.Alert">
|
||||
<style name="AppTheme.AlertDialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert">
|
||||
<item name="android:background">@drawable/bg_rounded</item>
|
||||
<item name="android:gravity">bottom</item>
|
||||
</style>
|
||||
|
|
6
app/src/main/res/xml/network_security_config.xml
Normal file
6
app/src/main/res/xml/network_security_config.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Used to allow connections to http://localhost on developer machine -->
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
|
@ -17,6 +17,7 @@ class AuthModule {
|
|||
SharedPreferencesCredentialsProvider(sharedPreferences)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedPreferences(context: Context): SharedPreferences =
|
||||
EncryptedSharedPreferences.create(
|
||||
ENCRYPTED_SHARED_PREFS_FILE_NAME,
|
||||
|
|
|
@ -3,8 +3,8 @@ package com.wbrawner.budget.auth
|
|||
import android.content.SharedPreferences
|
||||
|
||||
interface CredentialsProvider {
|
||||
fun username(): String
|
||||
fun password(): String
|
||||
val username: String
|
||||
val password: String
|
||||
fun saveCredentials(username: String, password: String)
|
||||
}
|
||||
|
||||
|
@ -12,9 +12,9 @@ private const val PREF_KEY_USERNAME = "PREF_KEY_USERNAME"
|
|||
private const val PREF_KEY_PASSWORD = "PREF_KEY_PASSWORD"
|
||||
|
||||
class SharedPreferencesCredentialsProvider(private val sharedPreferences: SharedPreferences) : CredentialsProvider {
|
||||
override fun username(): String = sharedPreferences.getString(PREF_KEY_USERNAME, null) ?: ""
|
||||
override val username: String = sharedPreferences.getString(PREF_KEY_USERNAME, null) ?: ""
|
||||
|
||||
override fun password(): String = sharedPreferences.getString(PREF_KEY_PASSWORD, null) ?: ""
|
||||
override val password: String = sharedPreferences.getString(PREF_KEY_PASSWORD, null) ?: ""
|
||||
|
||||
override fun saveCredentials(username: String, password: String) {
|
||||
sharedPreferences.edit()
|
||||
|
|
|
@ -28,8 +28,8 @@ dependencies {
|
|||
implementation project(':auth')
|
||||
// Retrofit
|
||||
api "com.squareup.retrofit2:retrofit:$rootProject.ext.retrofit"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.ext.retrofit"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:$rootProject.ext.retrofit"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
|
||||
api "com.squareup.moshi:moshi:$rootProject.ext.moshi"
|
||||
implementation "com.squareup.moshi:moshi-adapters:$rootProject.ext.moshi"
|
||||
// Dagger
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package com.wbrawner.budget.lib.network
|
||||
|
||||
data class AccountBalanceResponse(val id: Long, val balance: Long)
|
|
@ -0,0 +1,3 @@
|
|||
package com.wbrawner.budget.lib.network
|
||||
|
||||
data class BudgetBalanceResponse(val id: Long, val balance: Long)
|
|
@ -1,107 +1,115 @@
|
|||
package com.wbrawner.budget.lib.network
|
||||
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.transaction.Transaction
|
||||
import com.wbrawner.budget.common.user.LoginRequest
|
||||
import com.wbrawner.budget.common.user.User
|
||||
import io.reactivex.Single
|
||||
import com.wbrawner.budget.lib.repository.NewBudgetRequest
|
||||
import retrofit2.http.*
|
||||
|
||||
interface BudgetApiService {
|
||||
// Accounts
|
||||
@GET("accounts")
|
||||
fun getAccounts(
|
||||
// Budgets
|
||||
@GET("budgets")
|
||||
suspend fun getBudgets(
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Single<Collection<Account>>
|
||||
): Collection<Budget>
|
||||
|
||||
@GET("accounts/{id}")
|
||||
fun getAccount(@Path("id") id: Long): Single<Account>
|
||||
@GET("budgets/{id}")
|
||||
suspend fun getBudget(@Path("id") id: Long): Budget
|
||||
|
||||
@GET("accounts/{id}/balance")
|
||||
fun getAccountBalance(@Path("id") id: Long): Single<AccountBalanceResponse>
|
||||
@GET("budgets/{id}/balance")
|
||||
suspend fun getBudgetBalance(@Path("id") id: Long): BudgetBalanceResponse
|
||||
|
||||
@POST("accounts/new")
|
||||
fun newAccount(@Body account: Account): Single<Account>
|
||||
@POST("budgets/new")
|
||||
suspend fun newBudget(@Body budget: NewBudgetRequest): Budget
|
||||
|
||||
@PUT("accounts/{id}")
|
||||
fun updateAccount(
|
||||
@PUT("budgets/{id}")
|
||||
suspend fun updateBudget(
|
||||
@Path("id") id: Long,
|
||||
@Body account: Account
|
||||
): Single<Account>
|
||||
@Body budget: Budget
|
||||
): Budget
|
||||
|
||||
@DELETE("accounts/{id}")
|
||||
fun deleteAccount(@Path("id") id: Long): Single<Void>
|
||||
@DELETE("budgets/{id}")
|
||||
suspend fun deleteBudget(@Path("id") id: Long)
|
||||
|
||||
// Categories
|
||||
@GET("categories")
|
||||
fun getCategories(
|
||||
@Query("accountId") accountId: Long,
|
||||
suspend fun getCategories(
|
||||
@Query("budgetId") budgetId: Long? = null,
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Single<Collection<Category>>
|
||||
): Collection<Category>
|
||||
|
||||
@GET("categories/{id}")
|
||||
fun getCategory(@Path("id") id: Long): Single<Category>
|
||||
suspend fun getCategory(@Path("id") id: Long): Category
|
||||
|
||||
@GET("categories/{id}/balance")
|
||||
fun getCategoryBalance(@Path("id") id: Long): Single<CategoryBalanceResponse>
|
||||
suspend fun getCategoryBalance(@Path("id") id: Long): CategoryBalanceResponse
|
||||
|
||||
@POST("categories/new")
|
||||
fun newCategory(@Body category: Category): Single<Category>
|
||||
suspend fun newCategory(@Body category: Category): Category
|
||||
|
||||
@PUT("categories/{id}")
|
||||
fun updateCategory(
|
||||
suspend fun updateCategory(
|
||||
@Path("id") id: Long,
|
||||
@Body category: Category
|
||||
): Single<Category>
|
||||
): Category
|
||||
|
||||
@DELETE("categories/{id}")
|
||||
fun deleteCategory(@Path("id") id: Long): Single<Void>
|
||||
suspend fun deleteCategory(@Path("id") id: Long)
|
||||
|
||||
// Transactions
|
||||
@GET("transactions")
|
||||
fun getTransactions(
|
||||
@Query("accountId") accountId: Long,
|
||||
suspend fun getTransactions(
|
||||
@Query("budgetId") budgetId: Long? = null,
|
||||
@Query("categoryId") categoryId: Long? = null,
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Single<Collection<Transaction>>
|
||||
): Collection<Transaction>
|
||||
|
||||
@GET("transactions/{id}")
|
||||
fun getTransaction(@Path("id") id: Long): Single<Transaction>
|
||||
suspend fun getTransaction(@Path("id") id: Long): Transaction
|
||||
|
||||
@POST("transactions/new")
|
||||
fun newTransaction(@Body transaction: Transaction): Single<Transaction>
|
||||
suspend fun newTransaction(@Body transaction: Transaction): Transaction
|
||||
|
||||
@PUT("transactions/{id}")
|
||||
fun updateTransaction(
|
||||
suspend fun updateTransaction(
|
||||
@Path("id") id: Long,
|
||||
@Body transaction: Transaction
|
||||
): Single<Transaction>
|
||||
): Transaction
|
||||
|
||||
@DELETE("transactions/{id}")
|
||||
fun deleteTransaction(@Path("id") id: Long): Single<Void>
|
||||
suspend fun deleteTransaction(@Path("id") id: Long)
|
||||
|
||||
// Users
|
||||
@GET("users")
|
||||
fun getUsers(
|
||||
@Query("accountId") accountId: Long,
|
||||
suspend fun getUsers(
|
||||
@Query("budgetId") budgetId: Long? = null,
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Single<Collection<User>>
|
||||
): Collection<User>
|
||||
|
||||
@POST("users/login")
|
||||
suspend fun login(@Body request: LoginRequest): User
|
||||
|
||||
@GET("users/search")
|
||||
suspend fun searchUsers(@Query("query") query: String): List<User>
|
||||
|
||||
@GET("users/{id}")
|
||||
fun getUser(@Path("id") id: Long): Single<User>
|
||||
suspend fun getUser(@Path("id") id: Long): User
|
||||
|
||||
@POST("users/new")
|
||||
fun newUser(@Body user: User): Single<User>
|
||||
suspend fun newUser(@Body user: User): User
|
||||
|
||||
@PUT("users/{id}")
|
||||
fun updateUser(
|
||||
suspend fun updateUser(
|
||||
@Path("id") id: Long,
|
||||
@Body user: User
|
||||
): Single<User>
|
||||
): User
|
||||
|
||||
@DELETE("users/{id}")
|
||||
fun deleteUser(@Path("id") id: Long): Single<Void>
|
||||
suspend fun deleteUser(@Path("id") id: Long)
|
||||
}
|
|
@ -3,11 +3,11 @@ package com.wbrawner.budget.lib.network
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import com.wbrawner.budget.auth.CredentialsProvider
|
||||
import com.wbrawner.budget.common.account.AccountRepository
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.common.transaction.TransactionRepository
|
||||
import com.wbrawner.budget.common.user.UserRepository
|
||||
import com.wbrawner.budget.lib.repository.NetworkAccountRepository
|
||||
import com.wbrawner.budget.lib.repository.NetworkBudgetRepository
|
||||
import com.wbrawner.budget.lib.repository.NetworkCategoryRepository
|
||||
import com.wbrawner.budget.lib.repository.NetworkTransactionRepository
|
||||
import com.wbrawner.budget.lib.repository.NetworkUserRepository
|
||||
|
@ -16,7 +16,6 @@ import dagger.Provides
|
|||
import okhttp3.Credentials
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.util.*
|
||||
import javax.inject.Named
|
||||
|
@ -34,8 +33,8 @@ class NetworkModule {
|
|||
if (it.request().headers().get("Authorization") != null)
|
||||
return@addInterceptor it.proceed(it.request())
|
||||
val credentials = Credentials.basic(
|
||||
credentialsProvider.username(),
|
||||
credentialsProvider.password()
|
||||
credentialsProvider.username,
|
||||
credentialsProvider.password
|
||||
)
|
||||
val newHeaders = it.request()
|
||||
.headers()
|
||||
|
@ -57,7 +56,6 @@ class NetworkModule {
|
|||
client: OkHttpClient
|
||||
): Retrofit = Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.baseUrl(baseUrl)
|
||||
.client(client)
|
||||
.build()
|
||||
|
@ -67,8 +65,8 @@ class NetworkModule {
|
|||
retrofit.create(BudgetApiService::class.java)
|
||||
|
||||
@Provides
|
||||
fun provideAccountRepository(apiService: BudgetApiService): AccountRepository =
|
||||
NetworkAccountRepository(apiService)
|
||||
fun provideAccountRepository(apiService: BudgetApiService): BudgetRepository =
|
||||
NetworkBudgetRepository(apiService)
|
||||
|
||||
@Provides
|
||||
fun provideCategoryRepository(apiService: BudgetApiService): CategoryRepository =
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
package com.wbrawner.budget.lib.repository
|
||||
|
||||
import com.wbrawner.budget.common.account.Account
|
||||
import com.wbrawner.budget.common.account.AccountRepository
|
||||
import com.wbrawner.budget.lib.network.BudgetApiService
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class NetworkAccountRepository @Inject constructor(private val apiService: BudgetApiService) :
|
||||
AccountRepository {
|
||||
override fun create(newItem: Account): Single<Account> = apiService.newAccount(newItem)
|
||||
|
||||
override fun findAll(): Single<Collection<Account>> = apiService.getAccounts()
|
||||
|
||||
override fun findById(id: Long): Single<Account> = apiService.getAccount(id)
|
||||
|
||||
override fun update(updatedItem: Account): Single<Account> =
|
||||
apiService.updateAccount(updatedItem.id!!, updatedItem)
|
||||
|
||||
override fun delete(id: Long): Single<Void> = apiService.deleteAccount(id)
|
||||
|
||||
override fun getBalance(id: Long): Single<Long> = Single.create {
|
||||
apiService.getAccountBalance(id).subscribe { res, err ->
|
||||
if (err != null) {
|
||||
it.onError(err)
|
||||
} else {
|
||||
it.onSuccess(res.balance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.wbrawner.budget.lib.repository
|
||||
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.user.LoginRequest
|
||||
import com.wbrawner.budget.common.user.User
|
||||
import com.wbrawner.budget.lib.network.BudgetApiService
|
||||
import javax.inject.Inject
|
||||
|
||||
class NetworkBudgetRepository @Inject constructor(private val apiService: BudgetApiService) :
|
||||
BudgetRepository {
|
||||
override suspend fun create(newItem: Budget): Budget =
|
||||
apiService.newBudget(NewBudgetRequest(newItem))
|
||||
|
||||
override suspend fun findAll(): Collection<Budget> = apiService.getBudgets().sortedBy { it.name }
|
||||
|
||||
override suspend fun findById(id: Long): Budget = apiService.getBudget(id)
|
||||
|
||||
override suspend fun update(updatedItem: Budget): Budget =
|
||||
apiService.updateBudget(updatedItem.id!!, updatedItem)
|
||||
|
||||
override suspend fun delete(id: Long) = apiService.deleteBudget(id)
|
||||
|
||||
override suspend fun login(username: String, password: String): User =
|
||||
apiService.login(LoginRequest(username, password))
|
||||
|
||||
override suspend fun getBalance(id: Long): Long = apiService.getBudgetBalance(id).balance
|
||||
}
|
||||
|
||||
data class NewBudgetRequest(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val userIds: List<Long>
|
||||
) {
|
||||
constructor(budget: Budget) : this(budget.name, budget.description, budget.users.map { it.id!! })
|
||||
}
|
|
@ -3,45 +3,22 @@ package com.wbrawner.budget.lib.repository
|
|||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.lib.network.BudgetApiService
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
class NetworkCategoryRepository @Inject constructor(private val apiService: BudgetApiService) : CategoryRepository {
|
||||
override fun create(newItem: Category): Single<Category> = apiService.newCategory(newItem)
|
||||
override suspend fun create(newItem: Category): Category = apiService.newCategory(newItem)
|
||||
|
||||
override fun findAll(accountId: Long): Single<Collection<Category>> = Single.create { subscriber ->
|
||||
apiService.getCategories(accountId).subscribe { categories, error ->
|
||||
if (error != null) {
|
||||
subscriber.onError(error)
|
||||
} else {
|
||||
subscriber.onSuccess(categories.sortedBy { it.title })
|
||||
}
|
||||
}
|
||||
override suspend fun findAll(accountId: Long?): Collection<Category> = apiService.getCategories(accountId).sortedBy { it.title }
|
||||
|
||||
}
|
||||
override suspend fun findAll(): Collection<Category> = findAll(null)
|
||||
|
||||
/**
|
||||
* This will only return an empty list, since an accountId is required to get categories.
|
||||
* Pass a [Long] as the first (and only) parameter to denote the
|
||||
* [account ID][com.wbrawner.budget.common.account.Account.id] instead
|
||||
*/
|
||||
override fun findAll(): Single<Collection<Category>> = Single.just(ArrayList())
|
||||
override suspend fun findById(id: Long): Category = apiService.getCategory(id)
|
||||
|
||||
override fun findById(id: Long): Single<Category> = apiService.getCategory(id)
|
||||
|
||||
override fun update(updatedItem: Category): Single<Category> =
|
||||
override suspend fun update(updatedItem: Category): Category =
|
||||
apiService.updateCategory(updatedItem.id!!, updatedItem)
|
||||
|
||||
override fun delete(id: Long): Single<Void> = apiService.deleteCategory(id)
|
||||
override suspend fun delete(id: Long) = apiService.deleteCategory(id)
|
||||
|
||||
// TODO: Implement this method server-side and then here
|
||||
override fun getBalance(id: Long): Single<Long> = Single.create { subscriber ->
|
||||
apiService.getCategoryBalance(id).subscribe { res, err ->
|
||||
if (err != null) {
|
||||
subscriber.onError(err)
|
||||
} else {
|
||||
subscriber.onSuccess(res.balance)
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun getBalance(id: Long): Long = apiService.getCategoryBalance(id).balance
|
||||
}
|
||||
|
||||
|
|
|
@ -3,33 +3,20 @@ package com.wbrawner.budget.lib.repository
|
|||
import com.wbrawner.budget.common.transaction.Transaction
|
||||
import com.wbrawner.budget.common.transaction.TransactionRepository
|
||||
import com.wbrawner.budget.lib.network.BudgetApiService
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class NetworkTransactionRepository @Inject constructor(private val apiService: BudgetApiService) : TransactionRepository {
|
||||
override fun create(newItem: Transaction): Single<Transaction> = apiService.newTransaction(newItem)
|
||||
override suspend fun create(newItem: Transaction): Transaction = apiService.newTransaction(newItem)
|
||||
|
||||
override fun findAll(accountId: Long): Single<Collection<Transaction>> = Single.create { subscriber ->
|
||||
apiService.getTransactions(accountId).subscribe { transactions, error ->
|
||||
if (error != null) {
|
||||
subscriber.onError(error)
|
||||
} else {
|
||||
subscriber.onSuccess(transactions.sortedByDescending { it.date })
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun findAll(budgetId: Long?, categoryId: Long?): Collection<Transaction> =
|
||||
apiService.getTransactions(budgetId, categoryId).sortedByDescending { it.date }
|
||||
|
||||
/**
|
||||
* This will only return an empty list, since an accountId is required to get transactions.
|
||||
* Pass a [Long] as the first (and only) parameter to denote the
|
||||
* [account ID][com.wbrawner.budget.common.account.Account.id] instead
|
||||
*/
|
||||
override fun findAll(): Single<Collection<Transaction>> = Single.just(ArrayList())
|
||||
override suspend fun findAll(): Collection<Transaction> = findAll(null)
|
||||
|
||||
override fun findById(id: Long): Single<Transaction> = apiService.getTransaction(id)
|
||||
override suspend fun findById(id: Long): Transaction = apiService.getTransaction(id)
|
||||
|
||||
override fun update(updatedItem: Transaction): Single<Transaction> =
|
||||
override suspend fun update(updatedItem: Transaction): Transaction =
|
||||
apiService.updateTransaction(updatedItem.id!!, updatedItem)
|
||||
|
||||
override fun delete(id: Long): Single<Void> = apiService.deleteTransaction(id)
|
||||
override suspend fun delete(id: Long) = apiService.deleteTransaction(id)
|
||||
}
|
||||
|
|
|
@ -3,25 +3,22 @@ package com.wbrawner.budget.lib.repository
|
|||
import com.wbrawner.budget.common.user.User
|
||||
import com.wbrawner.budget.common.user.UserRepository
|
||||
import com.wbrawner.budget.lib.network.BudgetApiService
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class NetworkUserRepository @Inject constructor(private val apiService: BudgetApiService) : UserRepository {
|
||||
override fun create(newItem: User): Single<User> = apiService.newUser(newItem)
|
||||
override suspend fun create(newItem: User): User = apiService.newUser(newItem)
|
||||
|
||||
override fun findAll(accountId: Long): Single<Collection<User>> = apiService.getUsers(accountId)
|
||||
override suspend fun findAll(accountId: Long?): Collection<User> = apiService.getUsers(accountId)
|
||||
|
||||
/**
|
||||
* This will only return an empty list, since an accountId is required to get users.
|
||||
* Pass a [Long] as the first (and only) parameter to denote the
|
||||
* [account ID][com.wbrawner.budget.common.account.Account.id] instead
|
||||
*/
|
||||
override fun findAll(): Single<Collection<User>> = Single.just(ArrayList())
|
||||
override suspend fun findAll(): Collection<User> = findAll(null)
|
||||
|
||||
override fun findById(id: Long): Single<User> = apiService.getUser(id)
|
||||
override suspend fun findById(id: Long): User = apiService.getUser(id)
|
||||
|
||||
override fun update(updatedItem: User): Single<User> =
|
||||
override suspend fun findAllByNameLike(query: String): Collection<User> =
|
||||
apiService.searchUsers(query)
|
||||
|
||||
override suspend fun update(updatedItem: User): User =
|
||||
apiService.updateUser(updatedItem.id!!, updatedItem)
|
||||
|
||||
override fun delete(id: Long): Single<Void> = apiService.deleteUser(id)
|
||||
override suspend fun delete(id: Long) = apiService.deleteUser(id)
|
||||
}
|
12
build.gradle
12
build.gradle
|
@ -1,7 +1,7 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.31'
|
||||
ext.kotlin_version = '1.3.50'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
@ -10,10 +10,10 @@ buildscript {
|
|||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'io.fabric.tools:gradle:1.26.1'
|
||||
classpath 'com.google.gms:google-services:4.3.2'
|
||||
classpath 'io.fabric.tools:gradle:1.31.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,8 @@ allprojects {
|
|||
ext {
|
||||
dagger = '2.23.1'
|
||||
moshi = '1.8.0'
|
||||
retrofit = '2.5.0'
|
||||
rxjava = '2.2.8'
|
||||
retrofit = '2.6.0'
|
||||
hyperion = '0.9.27'
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
|
|
@ -27,11 +27,10 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api "io.reactivex.rxjava2:rxjava:$rootProject.ext.rxjava"
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
repositories {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package com.wbrawner.budget.common
|
||||
|
||||
import io.reactivex.Single
|
||||
|
||||
/**
|
||||
* Base interface for an entity repository that provides basic CRUD methods
|
||||
*
|
||||
|
@ -9,9 +7,9 @@ import io.reactivex.Single
|
|||
* @param K The type of the primary identifier for the object supported by this repository
|
||||
*/
|
||||
interface Repository<T, K> {
|
||||
fun create(newItem: T): Single<T>
|
||||
fun findAll(): Single<Collection<T>>
|
||||
fun findById(id: K): Single<T>
|
||||
fun update(updatedItem: T): Single<T>
|
||||
fun delete(id: K): Single<Void>
|
||||
suspend fun create(newItem: T): T
|
||||
suspend fun findAll(): Collection<T>
|
||||
suspend fun findById(id: K): T
|
||||
suspend fun update(updatedItem: T): T
|
||||
suspend fun delete(id: K)
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package com.wbrawner.budget.common.account
|
||||
|
||||
data class Account(
|
||||
val id: Long? = null,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val currencyCode: String
|
||||
)
|
|
@ -1,8 +0,0 @@
|
|||
package com.wbrawner.budget.common.account
|
||||
|
||||
import com.wbrawner.budget.common.Repository
|
||||
import io.reactivex.Single
|
||||
|
||||
interface AccountRepository : Repository<Account, Long> {
|
||||
fun getBalance(id: Long): Single<Long>
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.wbrawner.budget.common.budget
|
||||
|
||||
import com.wbrawner.budget.common.user.User
|
||||
|
||||
data class Budget(
|
||||
val id: Long? = null,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val users: List<User> = emptyList()
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.wbrawner.budget.common.budget
|
||||
|
||||
import com.wbrawner.budget.common.Repository
|
||||
import com.wbrawner.budget.common.user.User
|
||||
|
||||
interface BudgetRepository : Repository<Budget, Long> {
|
||||
suspend fun login(username: String, password: String): User
|
||||
suspend fun getBalance(id: Long): Long
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package com.wbrawner.budget.common.category
|
||||
|
||||
data class Category(
|
||||
val accountId: Long,
|
||||
val budgetId: Long,
|
||||
val id: Long? = null,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package com.wbrawner.budget.common.category
|
||||
|
||||
import com.wbrawner.budget.common.Repository
|
||||
import io.reactivex.Single
|
||||
|
||||
interface CategoryRepository : Repository<Category, Long> {
|
||||
fun findAll(accountId: Long): Single<Collection<Category>>
|
||||
fun getBalance(id: Long): Single<Long>
|
||||
suspend fun findAll(accountId: Long? = null): Collection<Category>
|
||||
suspend fun getBalance(id: Long): Long
|
||||
}
|
|
@ -9,7 +9,7 @@ data class Transaction(
|
|||
val description: String,
|
||||
val amount: Long,
|
||||
val categoryId: Long? = null,
|
||||
val accountId: Long,
|
||||
val budgetId: Long,
|
||||
val expense: Boolean,
|
||||
val createdBy: Long
|
||||
)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package com.wbrawner.budget.common.transaction
|
||||
|
||||
import com.wbrawner.budget.common.Repository
|
||||
import io.reactivex.Single
|
||||
|
||||
interface TransactionRepository : Repository<Transaction, Long> {
|
||||
fun findAll(accountId: Long): Single<Collection<Transaction>>
|
||||
suspend fun findAll(budgetId: Long? = null, categoryId: Long? = null): Collection<Transaction>
|
||||
}
|
|
@ -6,3 +6,8 @@ data class User(
|
|||
val email: String? = null,
|
||||
val avatar: String? = null
|
||||
)
|
||||
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
|
@ -1,8 +1,8 @@
|
|||
package com.wbrawner.budget.common.user
|
||||
|
||||
import com.wbrawner.budget.common.Repository
|
||||
import io.reactivex.Single
|
||||
|
||||
interface UserRepository : Repository<User, Long> {
|
||||
fun findAll(accountId: Long): Single<Collection<User>>
|
||||
suspend fun findAll(accountId: Long? = null): Collection<User>
|
||||
suspend fun findAllByNameLike(query: String): Collection<User>
|
||||
}
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
||||
|
|
Loading…
Reference in a new issue