Clean up code and fix some discrepancies between API and local models/requests
This commit is contained in:
parent
623f65ea43
commit
7f3f1d82bc
29 changed files with 217 additions and 89 deletions
|
@ -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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -46,6 +46,7 @@ class SplashActivity : AppCompatActivity(), CoroutineScope {
|
|||
R.id.loginFragment
|
||||
}
|
||||
navController.navigate(navId)
|
||||
if (navId == R.id.mainActivity) finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
10
app/src/main/res/drawable/ic_filter_list.xml
Normal file
10
app/src/main/res/drawable/ic_filter_list.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
18
app/src/main/res/drawable/ic_twigs_color.xml
Normal file
18
app/src/main/res/drawable/ic_twigs_color.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
9
app/src/main/res/menu/menu_transaction_list.xml
Normal file
9
app/src/main/res/menu/menu_transaction_list.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/filter"
|
||||
android:icon="@drawable/ic_filter_list"
|
||||
android:title="@string/action_filter"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
}
|
Loading…
Reference in a new issue