Clean up code and fix some discrepancies between API and local models/requests

This commit is contained in:
William Brawner 2020-06-04 15:13:10 -07:00
parent 623f65ea43
commit 7f3f1d82bc
29 changed files with 217 additions and 89 deletions

View file

@ -21,7 +21,7 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// buildConfigField "String", "API_URL", "\"http://192.168.86.163:8080/\""
// buildConfigField "String", "API_URL", "\"http://10.0.2.2:8080/\""
buildConfigField "String", "API_URL", "\"https://budget-api.intra.wbrawner.com/\""
buildConfigField "String", "API_URL", "\"https://api.twigs.brawner.dev/\""
}
buildTypes {
release {

View file

@ -12,7 +12,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="GoogleAppIndexingWarning">
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="n">
<activity
android:name=".ui.SplashActivity"
android:theme="@style/SplashTheme"
@ -27,7 +28,8 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<activity android:name=".ui.MainActivity" />
<activity android:name=".ui.MainActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".ui.transactions.AddEditTransactionActivity"
android:parentActivityName=".ui.MainActivity">

View file

@ -46,6 +46,7 @@ class SplashActivity : AppCompatActivity(), CoroutineScope {
R.id.loginFragment
}
navController.navigate(navId)
if (navId == R.id.mainActivity) finish()
}
}
}

View file

@ -1,7 +1,6 @@
package com.wbrawner.budget.ui.auth
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -9,10 +8,10 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.fragment.findNavController
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
@ -49,7 +48,7 @@ class LoginFragment : Fragment(), CoroutineScope {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.isLoading.observe(this, Observer { isLoading ->
viewModel.isLoading.observe(viewLifecycleOwner, Observer { isLoading ->
formPrompt.show(!isLoading)
usernameContainer.show(!isLoading)
passwordContainer.show(!isLoading)
@ -69,7 +68,8 @@ class LoginFragment : Fragment(), CoroutineScope {
try {
val user = viewModel.login(username.text.toString(), password.text.toString())
(requireActivity().application as AllowanceApplication).currentUser = user
startActivity(Intent(requireContext().applicationContext, MainActivity::class.java))
findNavController().navigate(R.id.mainActivity)
activity?.finish()
} catch (e: Exception) {
username.error = "Invalid username/password"
password.error = "Invalid username/password"

View file

@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
abstract class ListWithAddButtonFragment<T : LoadingViewModel> : Fragment(), CoroutineScope {
abstract class ListWithAddButtonFragment<T : LoadingViewModel, State: BindableState> : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
@Inject
lateinit var viewModelFactory: BudgetViewModelFactory
@ -30,7 +30,7 @@ abstract class ListWithAddButtonFragment<T : LoadingViewModel> : Fragment(), Cor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(viewModelClass)
viewModel = ViewModelProvider(this, viewModelFactory).get(viewModelClass)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
@ -40,7 +40,7 @@ abstract class ListWithAddButtonFragment<T : LoadingViewModel> : Fragment(), Cor
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.isLoading.observe(this, Observer {
viewModel.isLoading.observe(viewLifecycleOwner, Observer {
view.findViewById<ProgressBar?>(R.id.progressBar)?.show(it)
})
recyclerView.layoutManager = LinearLayoutManager(view.context)
@ -61,14 +61,14 @@ abstract class ListWithAddButtonFragment<T : LoadingViewModel> : Fragment(), Cor
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
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
recyclerView?.adapter = BindableAdapter(items, constructors)
recyclerView?.visibility = View.VISIBLE
noItemsTextView?.visibility = View.GONE
}
}
}
@ -88,7 +88,7 @@ abstract class ListWithAddButtonFragment<T : LoadingViewModel> : Fragment(), Cor
abstract fun addItem()
abstract suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>>
abstract suspend fun loadItems(): Pair<List<State>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<State>>>
}
abstract class LoadingViewModel : ViewModel() {

View file

@ -10,12 +10,12 @@ 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>>() {
class BindableAdapter<T : BindableState>(
private val items: List<T>,
private val constructors: Map<Int, (view: View) -> BindableViewHolder<T>>
) : RecyclerView.Adapter<BindableAdapter.BindableViewHolder<T>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
: BindableViewHolder<in BindableState> = constructors[viewType]
: BindableViewHolder<T> = constructors[viewType]
?.invoke(LayoutInflater.from(parent.context).inflate(viewType, parent, false))
?: throw IllegalStateException("Attempted to create ViewHolder without proper constructor provided")
@ -23,11 +23,11 @@ class BindableAdapter(
override fun getItemViewType(position: Int): Int = items[position].viewType
override fun onBindViewHolder(holder: BindableViewHolder<in BindableState>, position: Int) {
override fun onBindViewHolder(holder: BindableViewHolder<T>, position: Int) {
holder.onBind(items[position])
}
override fun onViewRecycled(holder: BindableViewHolder<in BindableState>) {
override fun onViewRecycled(holder: BindableViewHolder<T>) {
holder.onUnbind()
}

View file

@ -15,7 +15,7 @@ import com.wbrawner.budget.ui.base.BindableState
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
import kotlinx.coroutines.CoroutineScope
class BudgetListFragment : ListWithAddButtonFragment<BudgetViewModel>(), CoroutineScope {
class BudgetListFragment : ListWithAddButtonFragment<BudgetViewModel, BudgetState>(), CoroutineScope {
override val noItemsStringRes: Int = R.string.overview_no_data
override val viewModelClass: Class<BudgetViewModel> = BudgetViewModel::class.java
@ -24,12 +24,19 @@ class BudgetListFragment : ListWithAddButtonFragment<BudgetViewModel>(), Corouti
super.onCreate(savedInstanceState)
}
override suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>> {
override suspend fun loadItems(): Pair<List<BudgetState>, Map<Int, (view: View) -> BudgetViewHolder>> {
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> })
mapOf(BUDGET_VIEW to { v ->
BudgetViewHolder(v) { _, budget ->
val bundle = Bundle().apply {
putLong(EXTRA_BUDGET_ID, budget.id!!)
}
findNavController().navigate(R.id.categoryListFragment, bundle)
}
})
)
}
@ -46,7 +53,7 @@ class BudgetState(val budget: Budget) : BindableState {
class BudgetViewHolder(
itemView: View,
private val navController: NavController
private val budgetClickListener: (View, Budget) -> Unit
) : BindableAdapter.BindableViewHolder<BudgetState>(itemView) {
private val name: TextView = itemView.findViewById(R.id.budgetName)
private val description: TextView = itemView.findViewById(R.id.budgetDescription)
@ -62,10 +69,7 @@ class BudgetViewHolder(
description.text = budget.description
}
itemView.setOnClickListener {
val bundle = Bundle().apply {
putLong(EXTRA_BUDGET_ID, budget.id!!)
}
navController.navigate(R.id.categoryListFragment, bundle)
budgetClickListener(it, budget)
}
}
}

View file

@ -21,7 +21,7 @@ import com.wbrawner.budget.ui.transactions.TransactionViewHolder
/**
* A simple [Fragment] subclass.
*/
class CategoryFragment : ListWithAddButtonFragment<CategoryViewModel>() {
class CategoryFragment : ListWithAddButtonFragment<CategoryViewModel, TransactionState>() {
override val noItemsStringRes: Int = R.string.transactions_no_data
override val viewModelClass: Class<CategoryViewModel> = CategoryViewModel::class.java
@ -31,7 +31,7 @@ class CategoryFragment : ListWithAddButtonFragment<CategoryViewModel>() {
setHasOptionsMenu(true)
}
override suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>> {
override suspend fun loadItems(): Pair<List<TransactionState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<TransactionState>>> {
val categoryId = arguments?.getLong(EXTRA_CATEGORY_ID)
if (categoryId == null) {
findNavController().navigateUp()
@ -40,12 +40,12 @@ class CategoryFragment : ListWithAddButtonFragment<CategoryViewModel>() {
val category = viewModel.getCategory(categoryId)
activity?.title = category.title
// TODO: Add category details here as well
val items = ArrayList<BindableState>()
val items = ArrayList<TransactionState>()
items.addAll(viewModel.getTransactions(categoryId).map { TransactionState(it) })
return Pair(
items,
mapOf(TRANSACTION_VIEW to { v ->
TransactionViewHolder(v, findNavController()) as BindableAdapter.BindableViewHolder<in BindableState>
TransactionViewHolder(v, findNavController())
})
)
}

View file

@ -18,7 +18,7 @@ import com.wbrawner.budget.ui.base.BindableState
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
import kotlinx.coroutines.launch
class CategoryListFragment : ListWithAddButtonFragment<CategoryViewModel>() {
class CategoryListFragment : ListWithAddButtonFragment<CategoryViewModel, CategoryState>() {
override val noItemsStringRes: Int = R.string.categories_no_data
override val viewModelClass: Class<CategoryViewModel> = CategoryViewModel::class.java
@ -27,7 +27,7 @@ class CategoryListFragment : ListWithAddButtonFragment<CategoryViewModel>() {
super.onCreate(savedInstanceState)
}
override suspend fun loadItems(): Pair<List<BindableState>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<in BindableState>>> {
override suspend fun loadItems(): Pair<List<CategoryState>, Map<Int, (view: View) -> CategoryViewHolder>> {
val budgetId = arguments?.getLong(EXTRA_BUDGET_ID)
if (budgetId == null) {
findNavController().navigateUp()
@ -38,7 +38,7 @@ class CategoryListFragment : ListWithAddButtonFragment<CategoryViewModel>() {
return Pair(
viewModel.getCategories(budgetId).map { CategoryState(it) },
mapOf(CATEGORY_VIEW to { v ->
CategoryViewHolder(v, viewModel, findNavController()) as BindableAdapter.BindableViewHolder<in BindableState>
CategoryViewHolder(v, viewModel, findNavController())
})
)
}

View file

@ -17,7 +17,7 @@ class CategoryListViewModel @Inject constructor(
suspend fun getCategory(id: Long): Category = categoryRepo.findById(id)
suspend fun getCategories(budgetId: Long? = null): Collection<Category> =
categoryRepo.findAll(budgetId)
categoryRepo.findAll(budgetId?.let { arrayOf(it) })
suspend fun saveCategory(category: Category) = if (category.id == null) categoryRepo.create(category)
else categoryRepo.update(category)

View file

@ -35,7 +35,7 @@ class CategoryViewModel @Inject constructor(
}
suspend fun getCategories(budgetId: Long? = null): Collection<Category> = showLoader {
categoryRepo.findAll(budgetId)
categoryRepo.findAll(budgetId?.let { arrayOf(it) })
}
suspend fun saveCategory(category: Category) = showLoader {

View file

@ -16,7 +16,7 @@ class AddEditTransactionViewModel @Inject constructor(
private val categoryRepository: CategoryRepository,
private val transactionRepository: TransactionRepository
) : ViewModel() {
suspend fun getCategories(accountId: Long) = categoryRepository.findAll(accountId)
suspend fun getCategories(budgetId: Long) = categoryRepository.findAll(arrayOf(budgetId))
suspend fun getTransaction(id: Long) = transactionRepository.findById(id)

View file

@ -3,8 +3,14 @@ package com.wbrawner.budget.ui.transactions
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
@ -17,32 +23,71 @@ 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 kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
class TransactionListFragment : ListWithAddButtonFragment<TransactionListViewModel>() {
class TransactionListFragment : ListWithAddButtonFragment<TransactionListViewModel, TransactionState>() {
override val viewModelClass: Class<TransactionListViewModel> = TransactionListViewModel::class.java
override val noItemsStringRes: Int = R.string.transactions_no_data
override fun onCreate(savedInstanceState: Bundle?) {
(requireActivity().application as AllowanceApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_transaction_list, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId != R.id.filter) {
return super.onOptionsItemSelected(item)
}
val dialogView = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
}
// TODO: Launch a Google Drive-style search/filter screen
AlertDialog.Builder(requireContext())
.setTitle("Filter Transactions")
.setPositiveButton(R.string.action_submit) { _, _ ->
launch {
loadItems(
arguments?.getLong(EXTRA_BUDGET_ID),
arguments?.getLong(EXTRA_CATEGORY_ID)
)
}
}
.setNegativeButton(R.string.action_cancel) { _, _ ->
// Do nothing
}
.create()
.show()
return true
}
override fun addItem() {
startActivity(Intent(activity, AddEditTransactionActivity::class.java))
}
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>
})
)
}
override suspend fun loadItems(): Pair<List<TransactionState>, Map<Int, (View) -> TransactionViewHolder>> = loadItems(
arguments?.getLong(EXTRA_BUDGET_ID),
arguments?.getLong(EXTRA_CATEGORY_ID)
)
private suspend fun loadItems(
budgetId: Long?,
categoryId: Long?,
from: Calendar? = null,
to: Calendar? = null
): Pair<List<TransactionState>, Map<Int, (View) -> TransactionViewHolder>> = Pair(
viewModel.getTransactions(budgetId, categoryId, from, to).map { TransactionState(it) },
mapOf(TRANSACTION_VIEW to { v ->
TransactionViewHolder(v, findNavController())
})
)
companion object {
const val TAG_FRAGMENT = "transactions"

View file

@ -7,12 +7,18 @@ import com.wbrawner.budget.ui.base.LoadingViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import java.util.*
import javax.inject.Inject
class TransactionListViewModel @Inject constructor(private val transactionRepo: TransactionRepository) :
LoadingViewModel() {
suspend fun getTransactions(budgetId: Long? = null, categoryId: Long? = null) = showLoader {
transactionRepo.findAll(budgetId = budgetId, categoryId = categoryId)
suspend fun getTransactions(
budgetId: Long? = null,
categoryId: Long? = null,
start: Calendar? = null,
end: Calendar? = null
) = showLoader {
transactionRepo.findAll(budgetId, categoryId, start, end)
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
</vector>

View file

@ -9,10 +9,10 @@
<path
android:pathData="m74.798,72.259l0,14.924 0,6.993c0,23.605 18.905,42.982 42.329,43.75l0,0.027l7.463,0 1.445,0 1.971,0l0,3.874 0,14.456 0,11.467c0,9.885 4.738,19.192 12.731,25.008 7.993,5.816 18.305,7.46 27.711,4.419l-4.591,-14.201c-4.877,1.577 -10.194,0.728 -14.338,-2.288 -4.144,-3.015 -6.587,-7.813 -6.587,-12.939l0,-11.467l3.411,0 6.019,0 1.444,0l0,-0.024c23.425,-0.768 42.33,-20.145 42.33,-43.749l0,-7.463 0,-14.456l-7.463,0 -9.43,0 -1.445,0 -7.462,0l0,0.024c-11.392,0.374 -21.714,5.148 -29.313,12.67 -5.353,-17.472 -21.398,-30.374 -40.426,-30.999l0,-0.024l-1.444,0 -6.018,0 -10.874,0zM89.723,87.183l3.412,0 6.018,0c16.021,0 28.849,12.829 28.849,28.85l0,6.993l-1.967,0 -1.445,0 -6.018,0c-16.021,0 -28.849,-12.828 -28.849,-28.85zM171.781,105.513l6.017,0 1.445,0 1.968,0l0,6.994c0,16.021 -12.828,28.849 -28.849,28.849l-6.019,0 -3.411,0l0,-6.993c0,-16.021 12.827,-28.85 28.849,-28.85z"
android:strokeWidth=".586787"
android:fillColor="@color/colorTextPrimary" />
android:fillColor="#1a1a1a" />
<path
android:pathData="m89.723,87.183l0,6.993c0,16.022 12.828,28.85 28.849,28.85l6.018,0 1.445,0 1.967,0l0,-6.993c0,-16.021 -12.828,-28.85 -28.849,-28.85l-6.018,0z"
android:strokeWidth=".586787"
android:fillColor="@color/colorAccent" />
android:fillColor="#bfff00" />
</group>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="416.8205"
android:viewportHeight="416.82053">
<group
android:translateX="72.94359"
android:translateY="72.943596">
<path
android:pathData="m74.798,72.259l0,14.924 0,6.993c0,23.605 18.905,42.982 42.329,43.75l0,0.027l7.463,0 1.445,0 1.971,0l0,3.874 0,14.456 0,11.467c0,9.885 4.738,19.192 12.731,25.008 7.993,5.816 18.305,7.46 27.711,4.419l-4.591,-14.201c-4.877,1.577 -10.194,0.728 -14.338,-2.288 -4.144,-3.015 -6.587,-7.813 -6.587,-12.939l0,-11.467l3.411,0 6.019,0 1.444,0l0,-0.024c23.425,-0.768 42.33,-20.145 42.33,-43.749l0,-7.463 0,-14.456l-7.463,0 -9.43,0 -1.445,0 -7.462,0l0,0.024c-11.392,0.374 -21.714,5.148 -29.313,12.67 -5.353,-17.472 -21.398,-30.374 -40.426,-30.999l0,-0.024l-1.444,0 -6.018,0 -10.874,0zM89.723,87.183l3.412,0 6.018,0c16.021,0 28.849,12.829 28.849,28.85l0,6.993l-1.967,0 -1.445,0 -6.018,0c-16.021,0 -28.849,-12.828 -28.849,-28.85zM171.781,105.513l6.017,0 1.445,0 1.968,0l0,6.994c0,16.021 -12.828,28.849 -28.849,28.849l-6.019,0 -3.411,0l0,-6.993c0,-16.021 12.827,-28.85 28.849,-28.85z"
android:strokeWidth=".586787"
android:fillColor="@color/colorTextPrimary" />
<path
android:pathData="m89.723,87.183l0,6.993c0,16.022 12.828,28.85 28.849,28.85l6.018,0 1.445,0 1.967,0l0,-6.993c0,-16.021 -12.828,-28.85 -28.849,-28.85l-6.018,0z"
android:strokeWidth=".586787"
android:fillColor="@color/colorAccent" />
</group>
</vector>

View file

@ -22,18 +22,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/action_bar">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/container_edit_category_name"
style="@style/AppTheme.EditText.Container"
android:hint="@string/prompt_category_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:hint="@string/prompt_category_name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_category_name"
@ -44,10 +42,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/container_edit_category_amount"
style="@style/AppTheme.EditText.Container"
android:hint="@string/prompt_category_amount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/container_edit_category_name">
android:hint="@string/prompt_category_amount">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_category_amount"
@ -60,15 +55,12 @@
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" />
android:text="@string/prompt_budget" />
<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>
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -36,6 +36,7 @@
android:layout_margin="16dp"
android:clickable="true"
android:focusable="true"
app:backgroundTint="@color/colorPrimary"
app:srcCompat="@drawable/ic_add_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View 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/filter"
android:icon="@drawable/ic_filter_list"
android:title="@string/action_filter"
app:showAsAction="ifRoom" />
</menu>

View file

@ -3,8 +3,8 @@
<color name="colorBackgroundPrimary">#FFFFFFFF</color>
<color name="colorPrimary">#30d158</color>
<color name="colorPrimaryDark">#34c759</color>
<color name="colorAccent">#bfff00</color>
<color name="colorTextPrimary">#FF000000</color>
<color name="colorAccent">#30d158</color>
<color name="colorTextPrimary">#1a1a1a</color>
<color name="colorTextGreen">#388e3c</color>
<color name="colorTextRed">#d32f2f</color>
<color name="colorTextPrimaryInverted">#FFDADADA</color>

View file

@ -40,7 +40,6 @@
<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>
@ -50,4 +49,8 @@
<string name="title_edit_budget">Edit Budget</string>
<string name="title_edit">Edit</string>
<string name="description_logo">Twigs Logo</string>
<string name="action_filter">Filter</string>
<string name="action_cancel">Cancel</string>
<string name="prompt_month">Month</string>
<string name="prompt_year">Year</string>
</resources>

View file

@ -29,7 +29,8 @@ dependencies {
// Retrofit
api "com.squareup.retrofit2:retrofit:$rootProject.ext.retrofit"
implementation "com.squareup.retrofit2:converter-moshi:$rootProject.ext.retrofit"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
implementation("com.squareup.okhttp3:logging-interceptor:4.7.2")
api "com.squareup.moshi:moshi:$rootProject.ext.moshi"
implementation "com.squareup.moshi:moshi-adapters:$rootProject.ext.moshi"
// Dagger

View file

@ -37,7 +37,7 @@ interface BudgetApiService {
// Categories
@GET("categories")
suspend fun getCategories(
@Query("budgetId") budgetId: Long? = null,
@Query("budgetIds") budgetIds: Array<Long>? = null,
@Query("count") count: Int? = null,
@Query("page") page: Int? = null
): Collection<Category>
@ -65,6 +65,8 @@ interface BudgetApiService {
suspend fun getTransactions(
@Query("budgetId") budgetId: Long? = null,
@Query("categoryId") categoryId: Long? = null,
@Query("from") from: String? = null,
@Query("to") to: String? = null,
@Query("count") count: Int? = null,
@Query("page") page: Int? = null
): Collection<Transaction>

View file

@ -12,12 +12,14 @@ 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
import com.wbrawner.budgetlib.BuildConfig
import dagger.Module
import dagger.Provides
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.nio.charset.Charset
@ -34,10 +36,10 @@ class NetworkModule {
@Provides
fun provideCookieJar(sharedPreferences: SharedPreferences): CookieJar {
return object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
sharedPreferences.edit()
.putString(
url.host(),
url.host,
cookies.joinToString(separator = ",") {
Base64.encode(it.toString().toByteArray(), 0)
.toString(charset = Charset.forName("UTF-8"))
@ -47,7 +49,7 @@ class NetworkModule {
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return sharedPreferences.getString(url.host(), "")
return sharedPreferences.getString(url.host, "")
?.split(",")
?.mapNotNull {
Cookie.parse(
@ -64,7 +66,13 @@ class NetworkModule {
@Provides
fun provideOkHttpClient(cookieJar: CookieJar): OkHttpClient = OkHttpClient.Builder()
.cookieJar(cookieJar)
// TODO: Add Gander interceptor
.apply {
if (BuildConfig.DEBUG)
this.addInterceptor(
HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT)
.setLevel(HttpLoggingInterceptor.Level.BODY)
)
}
.build()
@Provides

View file

@ -7,7 +7,7 @@ import javax.inject.Inject
class NetworkCategoryRepository @Inject constructor(private val apiService: BudgetApiService) : CategoryRepository {
override suspend fun create(newItem: Category): Category = apiService.newCategory(newItem)
override suspend fun findAll(accountId: Long?): Collection<Category> = apiService.getCategories(accountId).sortedBy { it.title }
override suspend fun findAll(budgetIds: Array<Long>?): Collection<Category> = apiService.getCategories(budgetIds).sortedBy { it.title }
override suspend fun findAll(): Collection<Category> = findAll(null)

View file

@ -3,13 +3,33 @@ 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 java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.util.*
import javax.inject.Inject
class NetworkTransactionRepository @Inject constructor(private val apiService: BudgetApiService) : TransactionRepository {
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
override suspend fun create(newItem: Transaction): Transaction = apiService.newTransaction(newItem)
override suspend fun findAll(budgetId: Long?, categoryId: Long?): Collection<Transaction> =
apiService.getTransactions(budgetId, categoryId).sortedByDescending { it.date }
override suspend fun findAll(
budgetId: Long?,
categoryId: Long?,
start: Calendar?,
end: Calendar?
): Collection<Transaction> = apiService.getTransactions(
budgetId,
categoryId,
start?.let {
it.timeZone = TimeZone.getTimeZone("UTC")
dateFormatter.format(it.time)
},
end?.let {
it.timeZone = TimeZone.getTimeZone("UTC")
dateFormatter.format(it.time)
}
)
override suspend fun findAll(): Collection<Transaction> = findAll(null)

View file

@ -3,6 +3,6 @@ package com.wbrawner.budget.common.category
import com.wbrawner.budget.common.Repository
interface CategoryRepository : Repository<Category, Long> {
suspend fun findAll(accountId: Long? = null): Collection<Category>
suspend fun findAll(budgetIds: Array<Long>? = null): Collection<Category>
suspend fun getBalance(id: Long): Long
}

View file

@ -1,7 +1,13 @@
package com.wbrawner.budget.common.transaction
import com.wbrawner.budget.common.Repository
import java.util.*
interface TransactionRepository : Repository<Transaction, Long> {
suspend fun findAll(budgetId: Long? = null, categoryId: Long? = null): Collection<Transaction>
suspend fun findAll(
budgetId: Long? = null,
categoryId: Long? = null,
start: Calendar? = null,
end: Calendar? = null
): Collection<Transaction>
}