init
This commit is contained in:
@@ -98,4 +98,8 @@ dependencies {
|
|||||||
implementation("com.google.dagger:hilt-android:2.57.1")
|
implementation("com.google.dagger:hilt-android:2.57.1")
|
||||||
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
|
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
|
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
|
||||||
|
|
||||||
|
implementation("io.ktor:ktor-client-core:3.4.1")
|
||||||
|
implementation("io.ktor:ktor-client-okhttp:3.4.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0")
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,11 @@ import androidx.compose.material3.DrawerValue
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
@@ -28,6 +30,7 @@ import cc.n0th1ng.tripmoney.screens.settings.SettingsScreen
|
|||||||
import cc.n0th1ng.tripmoney.screens.statistics.StatisticsScreen
|
import cc.n0th1ng.tripmoney.screens.statistics.StatisticsScreen
|
||||||
import cc.n0th1ng.tripmoney.screens.trippicker.TripPickerScreen
|
import cc.n0th1ng.tripmoney.screens.trippicker.TripPickerScreen
|
||||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
|
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -49,6 +52,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationDrawer() {
|
fun NavigationDrawer() {
|
||||||
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val navBackStack by navController.currentBackStackEntryAsState()
|
val navBackStack by navController.currentBackStackEntryAsState()
|
||||||
val current = navBackStack?.destination?.route
|
val current = navBackStack?.destination?.route
|
||||||
@@ -74,7 +79,7 @@ fun NavigationDrawer() {
|
|||||||
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screens.TRIP_PICKER,
|
startDestination = if(currentTripId == -1) Screens.TRIP_PICKER else Screens.LIST_EXPENSE,
|
||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
composable(Screens.LIST_EXPENSE) {
|
composable(Screens.LIST_EXPENSE) {
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import androidx.room.Room
|
|||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
||||||
|
import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao
|
||||||
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
||||||
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.ExchangeRate
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.utils.Icons
|
import cc.n0th1ng.tripmoney.utils.Icons
|
||||||
@@ -26,11 +28,12 @@ import java.time.LocalDateTime
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Database(entities = [Trip::class, Expense::class, Category::class], version = 1)
|
@Database(entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], version = 1)
|
||||||
abstract class TripDatabase : RoomDatabase() {
|
abstract class TripDatabase : RoomDatabase() {
|
||||||
abstract fun tripDao(): TripDao
|
abstract fun tripDao(): TripDao
|
||||||
abstract fun expenseDao(): ExpenseDao
|
abstract fun expenseDao(): ExpenseDao
|
||||||
abstract fun categoryDao(): CategoryDao
|
abstract fun categoryDao(): CategoryDao
|
||||||
|
abstract fun exchangeRateDao(): ExchangeRateDao
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +77,12 @@ object DatabaseModule {
|
|||||||
fun provideCategoryDao(database: TripDatabase): CategoryDao {
|
fun provideCategoryDao(database: TripDatabase): CategoryDao {
|
||||||
return database.categoryDao()
|
return database.categoryDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideExchangeRateDao(database: TripDatabase): ExchangeRateDao {
|
||||||
|
return database.exchangeRateDao()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package cc.n0th1ng.tripmoney.data.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.ExchangeRate
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ExchangeRateDao {
|
||||||
|
@Upsert
|
||||||
|
suspend fun insert(exchangeRate: ExchangeRate)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM exchange_rate WHERE id = :id")
|
||||||
|
suspend fun getById(id: String): ExchangeRate?
|
||||||
|
|
||||||
|
@Query("DELETE FROM exchange_rate WHERE DATE(date) < :cutoffDate")
|
||||||
|
suspend fun deleteOldRates(cutoffDate: String)
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ interface TripDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM trip
|
SELECT * FROM trip
|
||||||
|
ORDER BY DATE(trip.start_date) DESC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun tripsPaged(): PagingSource<Int, Trip>
|
fun tripsPaged(): PagingSource<Int, Trip>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package cc.n0th1ng.tripmoney.data.entity
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity("exchange_rate")
|
||||||
|
data class ExchangeRate(
|
||||||
|
@PrimaryKey
|
||||||
|
val id: String,
|
||||||
|
val base: String,
|
||||||
|
val target: String,
|
||||||
|
val rate: Double,
|
||||||
|
val date: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun buildKey(base: String, target: String, date: String): String {
|
||||||
|
return "${base}_${target}_${date}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package cc.n0th1ng.tripmoney.data.repository
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
||||||
|
import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.ExchangeRate
|
||||||
|
import cc.n0th1ng.tripmoney.service.ExchangeService
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import java.time.LocalDate
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ExchangeRateRepository @Inject constructor(
|
||||||
|
private val exchangeRateDao: ExchangeRateDao,
|
||||||
|
private val exchangeService: ExchangeService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun save(exchangeRate: ExchangeRate) {
|
||||||
|
exchangeRateDao.insert(exchangeRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
suspend fun getRate(base: Currencies, target: Currencies, date: LocalDate): Double {
|
||||||
|
val id = ExchangeRate.buildKey(base.name, target.name, date.toString())
|
||||||
|
val cachedRate = exchangeRateDao.getById(id)
|
||||||
|
return if (cachedRate != null) {
|
||||||
|
cachedRate.rate
|
||||||
|
} else {
|
||||||
|
val rate = exchangeService.getRate(base, target, date)
|
||||||
|
exchangeRateDao.insert(
|
||||||
|
ExchangeRate(
|
||||||
|
id = id,
|
||||||
|
base = base.name,
|
||||||
|
target = target.name,
|
||||||
|
rate = rate,
|
||||||
|
date = date.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
clearOldRates()
|
||||||
|
rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private suspend fun clearOldRates(daysToKeep: Int = 180) {
|
||||||
|
val cutoffDate = LocalDate.now().minusDays(daysToKeep.toLong()).toString()
|
||||||
|
exchangeRateDao.deleteOldRates(cutoffDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
package cc.n0th1ng.tripmoney.screens.addexpense
|
package cc.n0th1ng.tripmoney.screens.addexpense
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -10,16 +15,21 @@ import androidx.compose.foundation.layout.aspectRatio
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -29,11 +39,17 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -50,26 +66,38 @@ import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
|||||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddExpenseBottomSheet(
|
fun AddExpenseBottomSheet(
|
||||||
onSave: (Expense) -> Unit,
|
onSave: (Expense) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
categories: List<Category>,
|
expenseDtoToEdit: ExpenseDto?,
|
||||||
expenseDtoToEdit: ExpenseDto?
|
state: SheetState,
|
||||||
|
// categories: List<Category> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
|
// val currentTripId = 1
|
||||||
|
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||||
|
if (categories.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var amount by remember {
|
var amount by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
|
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val dummyFocusRequester = remember { FocusRequester() }
|
||||||
var showCurrencyDialog by remember { mutableStateOf(false) }
|
var showCurrencyDialog by remember { mutableStateOf(false) }
|
||||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||||
var showDateTimePicker by remember { mutableStateOf(false) }
|
var showDateTimePicker by remember { mutableStateOf(false) }
|
||||||
@@ -88,21 +116,33 @@ fun AddExpenseBottomSheet(
|
|||||||
}
|
}
|
||||||
var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") }
|
var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") }
|
||||||
var enableSave by remember { mutableStateOf(expenseDtoToEdit != null) }
|
var enableSave by remember { mutableStateOf(expenseDtoToEdit != null) }
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState,
|
sheetState = state,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(0.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 15.dp),
|
||||||
|
text = stringResource(if (expenseDtoToEdit == null) R.string.add_expense else R.string.edit_expense),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 35.sp,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||||
Row(
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(9.dp)
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = amount.ifEmpty { "0.00" },
|
text = amount.ifEmpty { "0.00" },
|
||||||
@@ -111,25 +151,61 @@ fun AddExpenseBottomSheet(
|
|||||||
)
|
)
|
||||||
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
|
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(14.dp))
|
Row(
|
||||||
OutlinedButton(onClick = { showDateTimePicker = true }) {
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { showDateTimePicker = true },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
|
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
|
||||||
fontSize = 17.sp
|
fontSize = 17.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(14.dp))
|
CategoryButton(
|
||||||
CategoryButton(onClick = { showCategoryDialog = true }, category = category)
|
onClick = { showCategoryDialog = true },
|
||||||
Spacer(Modifier.height(14.dp))
|
category = category,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.height(50.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
|
||||||
) {
|
) {
|
||||||
NoteInput(note = note) { newNote -> note = newNote }
|
NoteInput(
|
||||||
SaveButton(
|
note = note,
|
||||||
enabled = enableSave,
|
onTextChange = { newNote -> note = newNote },
|
||||||
onClick = {
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
|
focusRequester = dummyFocusRequester
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(0.dp)
|
||||||
|
.focusRequester(dummyFocusRequester)
|
||||||
|
.focusable()
|
||||||
|
)
|
||||||
|
NumberKeyboard(
|
||||||
|
onNumberClick = { number ->
|
||||||
|
val newText = (if (amount == "0.00") "" else amount) + number
|
||||||
|
if (newText.isDoubleTwoDigitsAboveZero()) {
|
||||||
|
amount = newText
|
||||||
|
enableSave = true
|
||||||
|
} else if (amount == "0.00") {
|
||||||
|
enableSave = false
|
||||||
|
}
|
||||||
|
dummyFocusRequester.requestFocus()
|
||||||
|
},
|
||||||
|
onBackspaceClick = {
|
||||||
|
if (amount == "0.00") return@NumberKeyboard
|
||||||
|
amount = amount.safeSubstring(0, amount.length - 1)
|
||||||
|
enableSave = amount.isDoubleTwoDigitsAboveZero()
|
||||||
|
},
|
||||||
|
onSave = {
|
||||||
val expenseToSave = Expense(
|
val expenseToSave = Expense(
|
||||||
amount = amount.toDouble(),
|
amount = amount.toDouble(),
|
||||||
currency = currency,
|
currency = currency,
|
||||||
@@ -142,26 +218,8 @@ fun AddExpenseBottomSheet(
|
|||||||
if (expenseDtoToEdit == null) expenseToSave
|
if (expenseDtoToEdit == null) expenseToSave
|
||||||
else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
|
else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
|
||||||
)
|
)
|
||||||
}
|
}, enableSave = enableSave
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Spacer(Modifier.height(14.dp))
|
|
||||||
NumberKeyboard(
|
|
||||||
onNumberClick = { number ->
|
|
||||||
val newText = (if (amount == "0.00") "" else amount) + number
|
|
||||||
if (newText.isDoubleTwoDigitsAboveZero()) {
|
|
||||||
amount = newText
|
|
||||||
enableSave = true
|
|
||||||
} else if (amount == "0.00") {
|
|
||||||
enableSave = false
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
onBackspaceClick = {
|
|
||||||
if (amount == "0.00") return@NumberKeyboard
|
|
||||||
amount = amount.safeSubstring(0, amount.length - 1)
|
|
||||||
enableSave = amount.isDoubleTwoDigitsAboveZero()
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,29 +268,39 @@ fun String.isDoubleTwoDigitsAboveZero(): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoteInput(note: String, onTextChange: (String) -> Unit) {
|
fun NoteInput(note: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester) {
|
||||||
var text by remember { mutableStateOf(note) }
|
var text by remember { mutableStateOf(note) }
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
modifier = modifier,
|
||||||
label = { Text(stringResource(R.string.note)) }, value = note, onValueChange = { newText ->
|
label = { Text(stringResource(R.string.note)) }, value = note, onValueChange = { newText ->
|
||||||
text = newText
|
text = newText
|
||||||
onTextChange(text)
|
onTextChange(text)
|
||||||
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CurrencyButton(onClick: () -> Unit, text: String) {
|
fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) {
|
||||||
OutlinedButton(onClick = onClick) {
|
OutlinedButton(onClick = onClick, modifier = modifier) {
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryButton(onClick: () -> Unit, category: Category) {
|
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.fillMaxWidth(0.5f)
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
@@ -258,20 +326,13 @@ fun SaveButton(enabled: Boolean, onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun Preview() {
|
|
||||||
TripMoneyTheme(darkTheme = true) {
|
|
||||||
NumberKeyboard(onNumberClick = {}, onBackspaceClick = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NumberKeyboard(
|
fun NumberKeyboard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onNumberClick: (String) -> Unit,
|
onNumberClick: (String) -> Unit,
|
||||||
onBackspaceClick: () -> Unit
|
onBackspaceClick: () -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
enableSave: Boolean
|
||||||
) {
|
) {
|
||||||
val buttonModifier = Modifier
|
val buttonModifier = Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
@@ -285,91 +346,109 @@ fun NumberKeyboard(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("1") },
|
onClick = { onNumberClick("1") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("1", fontSize = 20.sp)
|
Text("1", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("2") },
|
onClick = { onNumberClick("2") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("2", fontSize = 20.sp)
|
Text("2", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("3") },
|
onClick = { onNumberClick("3") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("3", fontSize = 20.sp)
|
Text("3", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = { onNumberClick("") },
|
||||||
|
modifier = buttonModifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("+", fontSize = 20.sp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("4") },
|
onClick = { onNumberClick("4") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("4", fontSize = 20.sp)
|
Text("4", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("5") },
|
onClick = { onNumberClick("5") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("5", fontSize = 20.sp)
|
Text("5", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("6") },
|
onClick = { onNumberClick("6") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("6", fontSize = 20.sp)
|
Text("6", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = { onNumberClick("") },
|
||||||
|
modifier = buttonModifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("-", fontSize = 20.sp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("7") },
|
onClick = { onNumberClick("7") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("7", fontSize = 20.sp)
|
Text("7", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("8") },
|
onClick = { onNumberClick("8") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("8", fontSize = 20.sp)
|
Text("8", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("9") },
|
onClick = { onNumberClick("9") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("9", fontSize = 20.sp)
|
Text("9", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = { onNumberClick("") },
|
||||||
|
modifier = buttonModifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("*", fontSize = 20.sp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick(".") },
|
onClick = { onNumberClick(".") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(".", fontSize = 20.sp)
|
Text(".", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = { onNumberClick("0") },
|
onClick = { onNumberClick("0") },
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("0", fontSize = 20.sp)
|
Text("0", fontSize = 20.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
TextButton(
|
||||||
onClick = onBackspaceClick,
|
onClick = onBackspaceClick,
|
||||||
modifier = buttonModifier.weight(1f)
|
modifier = buttonModifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
@@ -378,6 +457,147 @@ fun NumberKeyboard(
|
|||||||
contentDescription = stringResource(R.string.backspace)
|
contentDescription = stringResource(R.string.backspace)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = onSave,
|
||||||
|
modifier = buttonModifier.weight(1f),
|
||||||
|
enabled = enableSave
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(R.string.backspace)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//@SuppressLint("CoroutineCreationDuringComposition")
|
||||||
|
//@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
//@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
//@Preview
|
||||||
|
//@Composable
|
||||||
|
//fun PreviewLight() {
|
||||||
|
// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
// CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
// sheetState.show()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// TripMoneyTheme {
|
||||||
|
// AddExpenseBottomSheet(
|
||||||
|
// {}, {}, null, sheetState,
|
||||||
|
// categories = listOf(
|
||||||
|
// Category(
|
||||||
|
// name = "Hotel",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
||||||
|
// color = "#B3E5FC"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Jedzenie",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
||||||
|
// color = "#C8E6C9"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Transport",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
||||||
|
// color = "#FFCDD2"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Rozrywka",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
||||||
|
// color = "#FFF9C4"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#E1BEE7"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy1",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#D7CCC8"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy2",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#BBDEFB"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy3",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#D1C4E9"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy4",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#DCEDC8"
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//@SuppressLint("CoroutineCreationDuringComposition")
|
||||||
|
//@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
//@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
//@Preview
|
||||||
|
//@Composable
|
||||||
|
//fun PreviewDark() {
|
||||||
|
// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
// CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
// sheetState.show()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// TripMoneyTheme(darkTheme = true) {
|
||||||
|
// AddExpenseBottomSheet(
|
||||||
|
// {}, {}, null, sheetState,
|
||||||
|
// categories = listOf(
|
||||||
|
// Category(
|
||||||
|
// name = "Hotel",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
||||||
|
// color = "#B3E5FC"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Jedzenie",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
||||||
|
// color = "#C8E6C9"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Transport",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
||||||
|
// color = "#FFCDD2"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Rozrywka",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
||||||
|
// color = "#FFF9C4"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#E1BEE7"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy1",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#D7CCC8"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy2",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#BBDEFB"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy3",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#D1C4E9"
|
||||||
|
// ),
|
||||||
|
// Category(
|
||||||
|
// name = "Zakupy4",
|
||||||
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
|
// color = "#DCEDC8"
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -5,6 +5,7 @@ import android.os.Build
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -33,6 +34,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.SwipeToDismissBox
|
import androidx.compose.material3.SwipeToDismissBox
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -56,10 +58,14 @@ import cc.n0th1ng.tripmoney.R.string
|
|||||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||||
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
||||||
|
import cc.n0th1ng.tripmoney.service.ExchangeService
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||||
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -71,8 +77,6 @@ fun ListExpenseScreen() {
|
|||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
|
|
||||||
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
||||||
val categories by expenseAndCategoryViewModel.getCategories()
|
|
||||||
.collectAsState(initial = emptyList())
|
|
||||||
val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems()
|
val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
@@ -98,27 +102,13 @@ fun ListExpenseScreen() {
|
|||||||
val expenseDto = expenses[index]
|
val expenseDto = expenses[index]
|
||||||
if (expenseDto != null) {
|
if (expenseDto != null) {
|
||||||
val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1)
|
val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1)
|
||||||
|
|
||||||
val showDayDivider =
|
val showDayDivider =
|
||||||
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
|
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
|
||||||
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
|
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
if (showDayDivider) {
|
if (showDayDivider) {
|
||||||
Row(
|
CustomDivider(expenseDto)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.Absolute.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
|
||||||
Text(
|
|
||||||
LocalDateTime.parse(expenseDto.expense.datetime).format(
|
|
||||||
DateTimeFormatter.ofPattern("dd EEEE")
|
|
||||||
).toString(),
|
|
||||||
modifier = Modifier.background(Color.White.copy(alpha = 0f))
|
|
||||||
)
|
|
||||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
SwipeToDeleteExpenseCard(
|
SwipeToDeleteExpenseCard(
|
||||||
@@ -143,13 +133,32 @@ fun ListExpenseScreen() {
|
|||||||
expenseDtoToEdit = null
|
expenseDtoToEdit = null
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
},
|
},
|
||||||
categories = categories,
|
expenseDtoToEdit = expenseDtoToEdit,
|
||||||
expenseDtoToEdit = expenseDtoToEdit
|
state = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Composable
|
||||||
|
fun CustomDivider(expenseDto: ExpenseDto) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Absolute.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
LocalDateTime.parse(expenseDto.expense.datetime).format(
|
||||||
|
DateTimeFormatter.ofPattern("dd EEEE")
|
||||||
|
).toString(),
|
||||||
|
modifier = Modifier.background(Color.White.copy(alpha = 0f))
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun SwipeToDeleteExpenseCard(
|
fun SwipeToDeleteExpenseCard(
|
||||||
@@ -257,7 +266,10 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.height(70.dp)
|
.height(70.dp)
|
||||||
.clickable { onClick(expenseDto) },
|
.combinedClickable(
|
||||||
|
enabled = true,
|
||||||
|
onClick = { onClick(expenseDto) },
|
||||||
|
onLongClick = { onClick(expenseDto) }),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -312,8 +324,16 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
|
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
|
||||||
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
|
val amount by
|
||||||
|
expenseAndCategoryViewModel.convertAmount(
|
||||||
|
amount = expenseDto.expense.amount,
|
||||||
|
base = Currencies.valueOf(expenseDto.expense.currency),
|
||||||
|
target = Currencies.valueOf(expenseDto.trip.currency),
|
||||||
|
date = LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate()
|
||||||
|
).collectAsState(initial = 0.0)
|
||||||
Text(
|
Text(
|
||||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.amount),
|
text = "≈ %.2f ${expenseDto.trip.currency}".format(amount),
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,27 @@ package cc.n0th1ng.tripmoney.screens.trippicker
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Shapes
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -23,31 +30,32 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import cc.n0th1ng.tripmoney.R
|
import cc.n0th1ng.tripmoney.R
|
||||||
import cc.n0th1ng.tripmoney.R.string
|
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton
|
import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton
|
||||||
import cc.n0th1ng.tripmoney.screens.addexpense.isDoubleTwoDigitsAboveZero
|
|
||||||
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
||||||
import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker
|
import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker
|
||||||
import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker
|
|
||||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import java.time.Instant
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit: Trip?) {
|
fun AddTripBottomSheet(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSave: (Trip) -> Unit,
|
||||||
|
tripToEdit: Trip?,
|
||||||
|
sheetState: SheetState
|
||||||
|
) {
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
var name by remember { mutableStateOf(tripToEdit?.name ?: "") }
|
var name by remember { mutableStateOf(tripToEdit?.name ?: "") }
|
||||||
var startDate by remember {
|
var startDate by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
@@ -65,31 +73,61 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.padding(16.dp),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
horizontalAlignment = Alignment.Start
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 15.dp),
|
||||||
|
text = stringResource(if(tripToEdit == null) R.string.add_trip else R.string.edit_trip),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 35.sp,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||||
NameInput(name = name, onTextChange = { newText ->
|
NameInput(name = name, onTextChange = { newText ->
|
||||||
name = newText
|
name = newText
|
||||||
enableSave = !name.isEmpty()
|
enableSave = !name.isEmpty()
|
||||||
})
|
})
|
||||||
CurrencyButton(onClick = {showCurrencyDialog = true}, currency)
|
Row(
|
||||||
OutlinedButton(onClick = { showDatePicker = true }) {
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
CurrencyButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(1f),
|
||||||
|
onClick = { showCurrencyDialog = true }, text = currency
|
||||||
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(1f)
|
||||||
|
.weight(1f),
|
||||||
|
onClick = { showDatePicker = true }) {
|
||||||
Text(
|
Text(
|
||||||
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
|
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
|
||||||
fontSize = 17.sp
|
fontSize = 17.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
enabled = enableSave,
|
enabled = enableSave,
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(Trip(name = name, startDate = startDate.toString(), currency = currency))
|
val trip = Trip(name = name, startDate = startDate.toString(), currency = currency)
|
||||||
|
|
||||||
|
onSave(if(tripToEdit == null) trip else trip.copy(id = tripToEdit.id))
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Check,
|
imageVector = Icons.Filled.Check,
|
||||||
contentDescription = stringResource(R.string.save)
|
contentDescription = stringResource(R.string.save)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +143,7 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showDatePicker) {
|
if (showDatePicker) {
|
||||||
DatePicker(startDate, onDismiss = {showDatePicker = false}, onConfirm = { newDate ->
|
DatePicker(startDate, onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
|
||||||
startDate = newDate
|
startDate = newDate
|
||||||
showDatePicker = false
|
showDatePicker = false
|
||||||
})
|
})
|
||||||
@@ -116,10 +154,10 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
|
|||||||
fun NameInput(name: String, onTextChange: (String) -> Unit) {
|
fun NameInput(name: String, onTextChange: (String) -> Unit) {
|
||||||
var text by remember { mutableStateOf(name) }
|
var text by remember { mutableStateOf(name) }
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
label = { Text(stringResource(R.string.name)) }, value = name, onValueChange = { newText ->
|
label = { Text(stringResource(R.string.name)) }, value = name, onValueChange = { newText ->
|
||||||
text = newText
|
text = newText
|
||||||
onTextChange(text)
|
onTextChange(text)
|
||||||
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.Build
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
@@ -28,6 +30,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.SwipeToDismissBox
|
import androidx.compose.material3.SwipeToDismissBox
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -39,6 +42,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -56,6 +61,7 @@ import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog
|
|||||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@@ -67,6 +73,7 @@ fun TripPickerScreen(
|
|||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems()
|
val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems()
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
|
var tripToEdit by remember { mutableStateOf<Trip?>(null) }
|
||||||
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { showBottomSheet = true }) {
|
onClick = { showBottomSheet = true }) {
|
||||||
@@ -83,12 +90,17 @@ fun TripPickerScreen(
|
|||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
val trip = trips[i]
|
val trip = trips[i]
|
||||||
if (trip != null) {
|
if (trip != null) {
|
||||||
SwipeToDeleteTripCard(trip, onDelete = {
|
SwipeToDeleteTripCard(
|
||||||
|
trip, onDelete = {
|
||||||
tripViewModel.delete(trip)
|
tripViewModel.delete(trip)
|
||||||
}, onClick = {
|
}, onClick = {
|
||||||
settingsViewModel.setCurrentTrip(trip.id)
|
settingsViewModel.setCurrentTrip(trip.id)
|
||||||
navController.navigate(Screens.LIST_EXPENSE)
|
navController.navigate(Screens.LIST_EXPENSE)
|
||||||
}, isSelected = currentTripId == trip.id)
|
}, isSelected = currentTripId == trip.id,
|
||||||
|
onLongClick = { trip ->
|
||||||
|
tripToEdit = trip
|
||||||
|
showBottomSheet = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
}
|
}
|
||||||
@@ -98,12 +110,15 @@ fun TripPickerScreen(
|
|||||||
AddTripBottomSheet(
|
AddTripBottomSheet(
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
|
tripToEdit = null
|
||||||
},
|
},
|
||||||
onSave = { trip ->
|
onSave = { trip ->
|
||||||
tripViewModel.save(trip)
|
tripViewModel.save(trip)
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
|
tripToEdit = null
|
||||||
},
|
},
|
||||||
null
|
tripToEdit = tripToEdit,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +127,8 @@ fun TripPickerScreen(
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun SwipeToDeleteTripCard(
|
fun SwipeToDeleteTripCard(
|
||||||
trip: Trip, onDelete: (Trip) -> Unit, onClick: (Trip) -> Unit, isSelected: Boolean
|
trip: Trip, onDelete: (Trip) -> Unit, onClick: (Trip) -> Unit, isSelected: Boolean,
|
||||||
|
onLongClick: (Trip) -> Unit
|
||||||
) {
|
) {
|
||||||
var dismissed by remember { mutableStateOf(false) }
|
var dismissed by remember { mutableStateOf(false) }
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
@@ -151,18 +167,27 @@ fun SwipeToDeleteTripCard(
|
|||||||
Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete))
|
Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete))
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
TripCard(trip, isSelected, onClick = onClick)
|
TripCard(trip, isSelected, onClick = onClick, onLongClick = onLongClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TripCard(trip: Trip, isSelected: Boolean, onClick: (Trip) -> Unit) {
|
fun TripCard(
|
||||||
|
trip: Trip,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: (Trip) -> Unit,
|
||||||
|
onLongClick: (Trip) -> Unit
|
||||||
|
) {
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(100.dp)
|
.height(100.dp)
|
||||||
.clickable(true, onClick = { onClick(trip) }),
|
.combinedClickable(enabled = true, onLongClick = {
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
onLongClick(trip)
|
||||||
|
}, onClick = { onClick(trip) }),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package cc.n0th1ng.tripmoney.service
|
||||||
|
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.double
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.time.LocalDate
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ExchangeService @Inject() constructor() {
|
||||||
|
private val API_URL: String = "https://api.frankfurter.dev"
|
||||||
|
private val client = HttpClient()
|
||||||
|
|
||||||
|
suspend fun getRate(base: Currencies, target: Currencies, date: LocalDate): Double {
|
||||||
|
return try {
|
||||||
|
val response = client.get("$API_URL/v1/$date") {
|
||||||
|
url {
|
||||||
|
parameters.append("base", base.name)
|
||||||
|
parameters.append("symbols", target.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val json = Json
|
||||||
|
json.parseToJsonElement(response.bodyAsText()).jsonObject["rates"]?.jsonObject[target.name]?.jsonPrimitive?.double
|
||||||
|
?: throw Exception("can not find rates")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("Error fetching exchange rate: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,16 @@ package cc.n0th1ng.tripmoney.theme
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Shapes
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = Purple80,
|
||||||
@@ -44,7 +47,6 @@ fun TripMoneyTheme(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,23 @@ import cc.n0th1ng.tripmoney.data.entity.Category
|
|||||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||||
import cc.n0th1ng.tripmoney.data.repository.CategoryRepository
|
import cc.n0th1ng.tripmoney.data.repository.CategoryRepository
|
||||||
|
import cc.n0th1ng.tripmoney.data.repository.ExchangeRateRepository
|
||||||
import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository
|
import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository
|
||||||
|
import cc.n0th1ng.tripmoney.service.ExchangeService
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import io.ktor.client.request.get
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.LocalDate
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ExpenseAndCategoryViewModel @Inject constructor(
|
class ExpenseAndCategoryViewModel @Inject constructor(
|
||||||
private val expenseRepo: ExpenseRepository,
|
private val expenseRepo: ExpenseRepository,
|
||||||
private val categoryRepo: CategoryRepository
|
private val categoryRepo: CategoryRepository,
|
||||||
|
private val exchangeRateRepository: ExchangeRateRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
fun getExpenses(tripId: Int): Flow<PagingData<ExpenseDto>> =
|
fun getExpenses(tripId: Int): Flow<PagingData<ExpenseDto>> =
|
||||||
@@ -43,4 +50,10 @@ class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
categoryRepo.save(category)
|
categoryRepo.save(category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun convertAmount(amount: Double, base: Currencies, target: Currencies, date: LocalDate): Flow<Double> {
|
||||||
|
return flow {
|
||||||
|
emit(amount * exchangeRateRepository.getRate(base, target, date))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
<string name="system_settings">Zgodnie z systemem</string>
|
<string name="system_settings">Zgodnie z systemem</string>
|
||||||
<string name="theme">Motyw</string>
|
<string name="theme">Motyw</string>
|
||||||
<string name="dark_theme">Ciemny motyw</string>
|
<string name="dark_theme">Ciemny motyw</string>
|
||||||
<string name="add_trip">Dodaj Wycieczkę</string>
|
<string name="add_trip">Dodaj wycieczkę</string>
|
||||||
<string name="name">Nazwa</string>
|
<string name="name">Nazwa</string>
|
||||||
|
<string name="edit_trip">Edytuj wycieczkę</string>
|
||||||
|
<string name="edit_expense">Edytuj wydatek</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -18,4 +18,6 @@
|
|||||||
<string name="system_settings">System settings</string>
|
<string name="system_settings">System settings</string>
|
||||||
<string name="add_trip">Add Trip</string>
|
<string name="add_trip">Add Trip</string>
|
||||||
<string name="name">Name</string>
|
<string name="name">Name</string>
|
||||||
|
<string name="edit_trip">Edit trip</string>
|
||||||
|
<string name="edit_expense">Edit expense</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user