init #48
@@ -98,4 +98,8 @@ dependencies {
|
||||
implementation("com.google.dagger:hilt-android:2.57.1")
|
||||
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
|
||||
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.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
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.trippicker.TripPickerScreen
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -49,6 +52,8 @@ class MainActivity : ComponentActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun NavigationDrawer() {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
val navController = rememberNavController()
|
||||
val navBackStack by navController.currentBackStackEntryAsState()
|
||||
val current = navBackStack?.destination?.route
|
||||
@@ -74,7 +79,7 @@ fun NavigationDrawer() {
|
||||
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screens.TRIP_PICKER,
|
||||
startDestination = if(currentTripId == -1) Screens.TRIP_PICKER else Screens.LIST_EXPENSE,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable(Screens.LIST_EXPENSE) {
|
||||
|
||||
@@ -8,9 +8,11 @@ import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
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.TripDao
|
||||
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.Trip
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
@@ -26,11 +28,12 @@ import java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
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 fun tripDao(): TripDao
|
||||
abstract fun expenseDao(): ExpenseDao
|
||||
abstract fun categoryDao(): CategoryDao
|
||||
abstract fun exchangeRateDao(): ExchangeRateDao
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +77,12 @@ object DatabaseModule {
|
||||
fun provideCategoryDao(database: TripDatabase): 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(
|
||||
"""
|
||||
SELECT * FROM trip
|
||||
ORDER BY DATE(trip.start_date) DESC
|
||||
"""
|
||||
)
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.height
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -29,11 +39,17 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.layout.ModifierLocalBeyondBoundsLayout
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.viewmodel.ExpenseAndCategoryViewModel
|
||||
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.format.DateTimeFormatter
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun AddExpenseBottomSheet(
|
||||
onSave: (Expense) -> 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 currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
// val currentTripId = 1
|
||||
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||
if (categories.isEmpty()) {
|
||||
return
|
||||
}
|
||||
var amount by remember {
|
||||
mutableStateOf(
|
||||
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
|
||||
)
|
||||
}
|
||||
val dummyFocusRequester = remember { FocusRequester() }
|
||||
var showCurrencyDialog by remember { mutableStateOf(false) }
|
||||
var showCategoryDialog 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 enableSave by remember { mutableStateOf(expenseDtoToEdit != null) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
sheetState = state,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(0.dp),
|
||||
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(
|
||||
modifier = Modifier.fillMaxWidth(0.9f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp)
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = amount.ifEmpty { "0.00" },
|
||||
@@ -111,41 +151,44 @@ fun AddExpenseBottomSheet(
|
||||
)
|
||||
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
OutlinedButton(onClick = { showDateTimePicker = true }) {
|
||||
Text(
|
||||
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
|
||||
fontSize = 17.sp
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
CategoryButton(onClick = { showCategoryDialog = true }, category = category)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
Row(
|
||||
modifier = Modifier.height(50.dp),
|
||||
modifier = Modifier.fillMaxWidth(0.9f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
NoteInput(note = note) { newNote -> note = newNote }
|
||||
SaveButton(
|
||||
enabled = enableSave,
|
||||
onClick = {
|
||||
val expenseToSave = Expense(
|
||||
amount = amount.toDouble(),
|
||||
currency = currency,
|
||||
note = note,
|
||||
datetime = datetime.toString(),
|
||||
categoryId = category.id,
|
||||
tripId = currentTripId
|
||||
)
|
||||
onSave(
|
||||
if (expenseDtoToEdit == null) expenseToSave
|
||||
else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { showDateTimePicker = true },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
|
||||
fontSize = 17.sp
|
||||
)
|
||||
}
|
||||
CategoryButton(
|
||||
onClick = { showCategoryDialog = true },
|
||||
category = category,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NoteInput(
|
||||
note = note,
|
||||
onTextChange = { newNote -> note = newNote },
|
||||
modifier = Modifier.fillMaxWidth(0.9f),
|
||||
focusRequester = dummyFocusRequester
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(0.dp)
|
||||
.focusRequester(dummyFocusRequester)
|
||||
.focusable()
|
||||
)
|
||||
NumberKeyboard(
|
||||
onNumberClick = { number ->
|
||||
val newText = (if (amount == "0.00") "" else amount) + number
|
||||
@@ -155,13 +198,28 @@ fun AddExpenseBottomSheet(
|
||||
} 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(
|
||||
amount = amount.toDouble(),
|
||||
currency = currency,
|
||||
note = note,
|
||||
datetime = datetime.toString(),
|
||||
categoryId = category.id,
|
||||
tripId = currentTripId
|
||||
)
|
||||
onSave(
|
||||
if (expenseDtoToEdit == null) expenseToSave
|
||||
else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
|
||||
)
|
||||
}, enableSave = enableSave
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -210,29 +268,39 @@ fun String.isDoubleTwoDigitsAboveZero(): Boolean {
|
||||
}
|
||||
|
||||
@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) }
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
label = { Text(stringResource(R.string.note)) }, value = note, onValueChange = { newText ->
|
||||
text = newText
|
||||
onTextChange(text)
|
||||
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrencyButton(onClick: () -> Unit, text: String) {
|
||||
OutlinedButton(onClick = onClick) {
|
||||
fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) {
|
||||
OutlinedButton(onClick = onClick, modifier = modifier) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryButton(onClick: () -> Unit, category: Category) {
|
||||
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
modifier = modifier
|
||||
) {
|
||||
Icon(
|
||||
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
|
||||
fun NumberKeyboard(
|
||||
modifier: Modifier = Modifier,
|
||||
onNumberClick: (String) -> Unit,
|
||||
onBackspaceClick: () -> Unit
|
||||
onBackspaceClick: () -> Unit,
|
||||
onSave: () -> Unit,
|
||||
enableSave: Boolean
|
||||
) {
|
||||
val buttonModifier = Modifier
|
||||
.padding(4.dp)
|
||||
@@ -285,91 +346,109 @@ fun NumberKeyboard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("1") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("1", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("2") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("2", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("3") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("3", fontSize = 20.sp)
|
||||
}
|
||||
TextButton(
|
||||
onClick = { onNumberClick("") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("+", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("4") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("4", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("5") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("5", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("6") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("6", fontSize = 20.sp)
|
||||
}
|
||||
TextButton(
|
||||
onClick = { onNumberClick("") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("-", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("7") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("7", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("8") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("8", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("9") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("9", fontSize = 20.sp)
|
||||
}
|
||||
TextButton(
|
||||
onClick = { onNumberClick("") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("*", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick(".") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text(".", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = { onNumberClick("0") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("0", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
TextButton(
|
||||
onClick = onBackspaceClick,
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
@@ -378,6 +457,147 @@ fun NumberKeyboard(
|
||||
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.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -33,6 +34,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SwipeToDismissBox
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.ExpenseDto
|
||||
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.SettingsViewModel
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -71,8 +77,6 @@ fun ListExpenseScreen() {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
|
||||
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
||||
val categories by expenseAndCategoryViewModel.getCategories()
|
||||
.collectAsState(initial = emptyList())
|
||||
val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems()
|
||||
val listState = rememberLazyListState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
@@ -98,27 +102,13 @@ fun ListExpenseScreen() {
|
||||
val expenseDto = expenses[index]
|
||||
if (expenseDto != null) {
|
||||
val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1)
|
||||
|
||||
val showDayDivider =
|
||||
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
|
||||
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
|
||||
.toLocalDate()
|
||||
Spacer(Modifier.height(5.dp))
|
||||
if (showDayDivider) {
|
||||
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))
|
||||
}
|
||||
CustomDivider(expenseDto)
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
SwipeToDeleteExpenseCard(
|
||||
@@ -143,13 +133,32 @@ fun ListExpenseScreen() {
|
||||
expenseDtoToEdit = null
|
||||
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)
|
||||
@Composable
|
||||
fun SwipeToDeleteExpenseCard(
|
||||
@@ -257,7 +266,10 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(70.dp)
|
||||
.clickable { onClick(expenseDto) },
|
||||
.combinedClickable(
|
||||
enabled = true,
|
||||
onClick = { onClick(expenseDto) },
|
||||
onLongClick = { onClick(expenseDto) }),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
|
||||
) {
|
||||
Row(
|
||||
@@ -312,8 +324,16 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
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 = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.amount),
|
||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(amount),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,20 +2,27 @@ package cc.n0th1ng.tripmoney.screens.trippicker
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -23,31 +30,32 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
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.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.R.string
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
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.DatePicker
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@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 startDate by remember {
|
||||
mutableStateOf(
|
||||
@@ -65,31 +73,61 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
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 ->
|
||||
name = newText
|
||||
enableSave = !name.isEmpty()
|
||||
})
|
||||
CurrencyButton(onClick = {showCurrencyDialog = true}, currency)
|
||||
OutlinedButton(onClick = { showDatePicker = true }) {
|
||||
Text(
|
||||
text = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
|
||||
fontSize = 17.sp
|
||||
Row(
|
||||
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 = startDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")),
|
||||
fontSize = 17.sp
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
OutlinedButton(
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(0.9f),
|
||||
enabled = enableSave,
|
||||
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(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = stringResource(R.string.save)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +143,7 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
|
||||
}
|
||||
|
||||
if (showDatePicker) {
|
||||
DatePicker(startDate, onDismiss = {showDatePicker = false}, onConfirm = { newDate ->
|
||||
DatePicker(startDate, onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
|
||||
startDate = newDate
|
||||
showDatePicker = false
|
||||
})
|
||||
@@ -116,10 +154,10 @@ fun AddTripBottomSheet(onDismiss: () -> Unit, onSave: (Trip) -> Unit, tripToEdit
|
||||
fun NameInput(name: String, onTextChange: (String) -> Unit) {
|
||||
var text by remember { mutableStateOf(name) }
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(0.9f),
|
||||
label = { Text(stringResource(R.string.name)) }, value = name, onValueChange = { newText ->
|
||||
text = newText
|
||||
onTextChange(text)
|
||||
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
@@ -28,6 +30,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SwipeToDismissBox
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -39,6 +42,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
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.text.font.FontWeight
|
||||
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.TripViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@@ -67,6 +73,7 @@ fun TripPickerScreen(
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems()
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
var tripToEdit by remember { mutableStateOf<Trip?>(null) }
|
||||
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { showBottomSheet = true }) {
|
||||
@@ -83,12 +90,17 @@ fun TripPickerScreen(
|
||||
Spacer(Modifier.height(10.dp))
|
||||
val trip = trips[i]
|
||||
if (trip != null) {
|
||||
SwipeToDeleteTripCard(trip, onDelete = {
|
||||
SwipeToDeleteTripCard(
|
||||
trip, onDelete = {
|
||||
tripViewModel.delete(trip)
|
||||
}, onClick = {
|
||||
settingsViewModel.setCurrentTrip(trip.id)
|
||||
navController.navigate(Screens.LIST_EXPENSE)
|
||||
}, isSelected = currentTripId == trip.id)
|
||||
}, isSelected = currentTripId == trip.id,
|
||||
onLongClick = { trip ->
|
||||
tripToEdit = trip
|
||||
showBottomSheet = true
|
||||
})
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
@@ -98,12 +110,15 @@ fun TripPickerScreen(
|
||||
AddTripBottomSheet(
|
||||
onDismiss = {
|
||||
showBottomSheet = false
|
||||
tripToEdit = null
|
||||
},
|
||||
onSave = { trip ->
|
||||
tripViewModel.save(trip)
|
||||
showBottomSheet = false
|
||||
tripToEdit = null
|
||||
},
|
||||
null
|
||||
tripToEdit = tripToEdit,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -112,7 +127,8 @@ fun TripPickerScreen(
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
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 showDialog by remember { mutableStateOf(false) }
|
||||
@@ -151,18 +167,27 @@ fun SwipeToDeleteTripCard(
|
||||
Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete))
|
||||
}
|
||||
}) {
|
||||
TripCard(trip, isSelected, onClick = onClick)
|
||||
TripCard(trip, isSelected, onClick = onClick, onLongClick = onLongClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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(
|
||||
modifier = Modifier
|
||||
.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)
|
||||
) {
|
||||
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 androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
@@ -44,7 +47,6 @@ fun TripMoneyTheme(
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
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.ExpenseDto
|
||||
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.service.ExchangeService
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
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.launch
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
private val expenseRepo: ExpenseRepository,
|
||||
private val categoryRepo: CategoryRepository
|
||||
private val categoryRepo: CategoryRepository,
|
||||
private val exchangeRateRepository: ExchangeRateRepository
|
||||
) : ViewModel() {
|
||||
|
||||
fun getExpenses(tripId: Int): Flow<PagingData<ExpenseDto>> =
|
||||
@@ -43,4 +50,10 @@ class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
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="theme">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="edit_trip">Edytuj wycieczkę</string>
|
||||
<string name="edit_expense">Edytuj wydatek</string>
|
||||
</resources>
|
||||
@@ -18,4 +18,6 @@
|
||||
<string name="system_settings">System settings</string>
|
||||
<string name="add_trip">Add Trip</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="edit_trip">Edit trip</string>
|
||||
<string name="edit_expense">Edit expense</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user