init #48
@@ -4,6 +4,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
id("com.google.dagger.hilt.android")
|
id("com.google.dagger.hilt.android")
|
||||||
|
alias(libs.plugins.baselineprofile)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -22,7 +23,8 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@@ -54,11 +56,13 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.compose.foundation.layout)
|
implementation(libs.androidx.compose.foundation.layout)
|
||||||
|
implementation(libs.androidx.profileinstaller)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
|
"baselineProfile"(project(":baselineprofile"))
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,19 @@ import androidx.compose.material3.rememberDrawerState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
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
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
||||||
import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer
|
import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer
|
||||||
@@ -27,6 +33,7 @@ import cc.n0th1ng.tripmoney.navigation.Screens
|
|||||||
import cc.n0th1ng.tripmoney.navigation.TopBar
|
import cc.n0th1ng.tripmoney.navigation.TopBar
|
||||||
import cc.n0th1ng.tripmoney.navigation.TopBarSettings
|
import cc.n0th1ng.tripmoney.navigation.TopBarSettings
|
||||||
import cc.n0th1ng.tripmoney.screens.listexpense.ListExpenseScreen
|
import cc.n0th1ng.tripmoney.screens.listexpense.ListExpenseScreen
|
||||||
|
import cc.n0th1ng.tripmoney.screens.managecategories.ManageCategoriesScreen
|
||||||
import cc.n0th1ng.tripmoney.screens.settings.SettingsScreen
|
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
|
||||||
@@ -40,21 +47,25 @@ import kotlinx.coroutines.launch
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
TripMoneyTheme {
|
TripMoneyTheme {
|
||||||
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
expenseAndCategoryViewModel.clearOldRates()
|
expenseAndCategoryViewModel.clearOldRates()
|
||||||
|
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId)
|
||||||
|
.collectAsLazyPagingItems()
|
||||||
NavigationDrawer()
|
NavigationDrawer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationDrawer() {
|
fun NavigationDrawer() {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
@@ -66,15 +77,19 @@ fun NavigationDrawer() {
|
|||||||
val current = navBackStack?.destination?.route
|
val current = navBackStack?.destination?.route
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
var filter by remember { mutableStateOf("") }
|
||||||
|
|
||||||
CustomNavigationDrawer(navController, drawerState) {
|
CustomNavigationDrawer(navController, drawerState) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
testTagsAsResourceId = true
|
||||||
|
},
|
||||||
topBar = {
|
topBar = {
|
||||||
if (current == Screens.SETTINGS) TopBarSettings(
|
if (current == Screens.SETTINGS) TopBarSettings(
|
||||||
navController
|
navController
|
||||||
) else TopBar(
|
) else TopBar(
|
||||||
title = currentTrip?.name ?: "",
|
title = currentTrip?.name ?: "",
|
||||||
onClick = {
|
onDrawerClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (drawerState.isClosed) {
|
if (drawerState.isClosed) {
|
||||||
drawerState.open()
|
drawerState.open()
|
||||||
@@ -82,7 +97,9 @@ fun NavigationDrawer() {
|
|||||||
drawerState.close()
|
drawerState.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
isSearchable = current == Screens.LIST_EXPENSE,
|
||||||
|
onFilterChange = { newFilter -> filter = newFilter})
|
||||||
},
|
},
|
||||||
|
|
||||||
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
||||||
@@ -92,7 +109,7 @@ fun NavigationDrawer() {
|
|||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
composable(Screens.LIST_EXPENSE) {
|
composable(Screens.LIST_EXPENSE) {
|
||||||
ListExpenseScreen()
|
ListExpenseScreen(filter)
|
||||||
}
|
}
|
||||||
composable(Screens.TRIP_PICKER) {
|
composable(Screens.TRIP_PICKER) {
|
||||||
TripPickerScreen(navController)
|
TripPickerScreen(navController)
|
||||||
@@ -101,7 +118,10 @@ fun NavigationDrawer() {
|
|||||||
StatisticsScreen()
|
StatisticsScreen()
|
||||||
}
|
}
|
||||||
composable(Screens.SETTINGS) {
|
composable(Screens.SETTINGS) {
|
||||||
SettingsScreen()
|
SettingsScreen(navController)
|
||||||
|
}
|
||||||
|
composable(Screens.MANAGE_CATEGORIES) {
|
||||||
|
ManageCategoriesScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.room.Database
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
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.ExchangeRateDao
|
||||||
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
||||||
@@ -15,24 +14,34 @@ 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.ExchangeRate
|
||||||
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.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
|
import cc.n0th1ng.tripmoney.screens.listexpense.toEpochMilli
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import cc.n0th1ng.tripmoney.utils.Icons
|
import cc.n0th1ng.tripmoney.utils.Icons
|
||||||
import cc.n0th1ng.tripmoney.utils.colors
|
import cc.n0th1ng.tripmoney.utils.colors
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Delay
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import javax.inject.Inject
|
import java.time.ZoneOffset
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.random.nextInt
|
||||||
|
|
||||||
@Database(entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], version = 1)
|
@Database(
|
||||||
|
entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class],
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class TripDatabase : RoomDatabase() {
|
abstract class TripDatabase : RoomDatabase() {
|
||||||
abstract fun tripDao(): TripDao
|
abstract fun tripDao(): TripDao
|
||||||
@@ -46,21 +55,28 @@ abstract class TripDatabase : RoomDatabase() {
|
|||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object DatabaseModule {
|
object DatabaseModule {
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideTripDatabase(
|
fun provideTripDatabase(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context
|
||||||
// expenseAndCategoryViewModel: ExpenseAndCategoryViewModel
|
|
||||||
): TripDatabase {
|
): TripDatabase {
|
||||||
|
|
||||||
val db: TripDatabase = Room.inMemoryDatabaseBuilder(
|
val db: TripDatabase = Room.inMemoryDatabaseBuilder(
|
||||||
context, TripDatabase::class.java
|
// val db: TripDatabase = Room.databaseBuilder(
|
||||||
).allowMainThreadQueries().build()
|
// name = "tripmoney_db",
|
||||||
|
context = context,
|
||||||
|
klass = TripDatabase::class.java,
|
||||||
|
)
|
||||||
|
.allowMainThreadQueries() // TODO Remove in production!
|
||||||
|
.fallbackToDestructiveMigration() // TODO Handle schema changes during dev
|
||||||
|
.build()
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
DatabasePrepopulator(
|
DatabasePrepopulator(
|
||||||
tripDao = db.tripDao(), categoryDao = db.categoryDao(), expenseDao = db.expenseDao()
|
tripDao = db.tripDao(),
|
||||||
|
categoryDao = db.categoryDao(),
|
||||||
|
expenseDao = db.expenseDao()
|
||||||
).prepopulate()
|
).prepopulate()
|
||||||
}
|
}
|
||||||
return db
|
return db
|
||||||
@@ -99,169 +115,92 @@ private class DatabasePrepopulator(
|
|||||||
) {
|
) {
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
suspend fun prepopulate() {
|
suspend fun prepopulate() {
|
||||||
tripDao.insert(Trip(name = "Włochy", startDate = LocalDate.parse("2026-03-01"), currency = "PLN"))
|
|
||||||
tripDao.insert(Trip(name = "Szwajcaria", startDate =LocalDate.parse("2025-03-01"), currency = "EUR"))
|
tripDao.insert(
|
||||||
tripDao.insert(Trip(name = "Portugalia", startDate = LocalDate.parse("2025-03-01"), currency = "USD"))
|
Trip(
|
||||||
categoryDao.insert(Category(name = "Accomodation", icon = Icons.HOTEL, color = colors.random()))
|
name = "Włochy",
|
||||||
categoryDao.insert(Category(name = "Transport", icon = Icons.TRANSPORT, color = colors.random()))
|
startDate = LocalDate.parse("2026-03-01"),
|
||||||
categoryDao.insert(Category(name = "Flight", icon = Icons.FLIGHT, color = colors.random()))
|
currency = "PLN"
|
||||||
categoryDao.insert(Category(name = "Restaurants", icon = Icons.RESTAURANT, color = colors.random()))
|
)
|
||||||
categoryDao.insert(Category(name = "Groceries", icon = Icons.GROCERIES, color = colors.random()))
|
)
|
||||||
categoryDao.insert(Category(name = "Coffee", icon = Icons.COFFEE,color = colors.random()))
|
tripDao.insert(
|
||||||
categoryDao.insert(Category(name = "Entertainment", icon = Icons.ENTERTAINMENT,color = colors.random()))
|
Trip(
|
||||||
categoryDao.insert(Category(name = "Laundry", icon = Icons.LAUNDRY,color = colors.random()))
|
name = "Szwajcaria",
|
||||||
|
startDate = LocalDate.parse("2025-03-01"),
|
||||||
|
currency = "EUR"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tripDao.insert(
|
||||||
|
Trip(
|
||||||
|
name = "Portugalia",
|
||||||
|
startDate = LocalDate.parse("2025-03-01"),
|
||||||
|
currency = "USD"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for (category in sampleCategories) {
|
||||||
|
categoryDao.insert(category)
|
||||||
|
}
|
||||||
|
for (expense in sampleExpenses) {
|
||||||
|
expenseDao.insert(expense)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val sampleCategories = listOf(
|
||||||
|
Category(
|
||||||
|
name = "Hotel",
|
||||||
|
icon = Icons.HOTEL,
|
||||||
|
color = colors.random()
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
name = "Jedzenie",
|
||||||
|
icon = Icons.RESTAURANT,
|
||||||
|
color = colors.random()
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
name = "Transport",
|
||||||
|
icon = Icons.FLIGHT,
|
||||||
|
color = colors.random()
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
name = "Rozrywka",
|
||||||
|
icon = Icons.ATTRACTION,
|
||||||
|
color = colors.random()
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
name = "Zakupy",
|
||||||
|
icon = Icons.GROCERIES,
|
||||||
|
color = colors.random()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
val sampleExpenses = (0..150).map { i ->
|
||||||
|
|
||||||
|
val datetime = if (i > 4) {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
expenseDao.insert(
|
val min = now.minusDays(10).toInstant(ZoneOffset.UTC).toEpochMilli()
|
||||||
Expense(
|
val max = now.toInstant(ZoneOffset.UTC).toEpochMilli()
|
||||||
amount = 120.50,
|
val randomMillis = Random.nextLong(min, max)
|
||||||
currency = "PLN",
|
LocalDateTime.ofInstant(Instant.ofEpochMilli(randomMillis), ZoneOffset.UTC)
|
||||||
note = "Hotel overnight",
|
} else {
|
||||||
datetime = now.minusDays(10),
|
LocalDateTime.now()
|
||||||
categoryId = 1,
|
}
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
val expense = Expense(
|
||||||
expenseDao.insert(
|
categoryId = Random.nextInt(1, 5),
|
||||||
Expense(
|
tripId = 1,
|
||||||
amount = 45.75,
|
amount = Random.nextDouble(0.1, 300.0),
|
||||||
currency = "PLN",
|
currency = Currencies.entries.random().name,
|
||||||
note = "Dinner",
|
note = if (i % 3 == 0) "Some note" else "",
|
||||||
datetime = now.minusDays(9),
|
datetime = datetime,
|
||||||
categoryId = 2,
|
rate = if (Random.nextBoolean()) Random.nextDouble(
|
||||||
tripId = 1
|
0.1,
|
||||||
)
|
5.0
|
||||||
)
|
) else 1.0
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 15.20,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Bus ticket",
|
|
||||||
datetime = now.minusDays(8),
|
|
||||||
categoryId = 3,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 89.99,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Concert tickets",
|
|
||||||
datetime = now.minusDays(7),
|
|
||||||
categoryId = 4,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 32.50,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Souvenirs",
|
|
||||||
datetime = now.minusDays(6),
|
|
||||||
categoryId = 5,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 180.00,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Hotel 3 nights",
|
|
||||||
datetime = now.minusDays(5),
|
|
||||||
categoryId = 1,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 67.30,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Lunch",
|
|
||||||
datetime = now.minusDays(4),
|
|
||||||
categoryId = 2,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 22.00,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Train ticket",
|
|
||||||
datetime = now.minusDays(3),
|
|
||||||
categoryId = 3,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 55.00,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Museum entry",
|
|
||||||
datetime = now.minusDays(2),
|
|
||||||
categoryId = 4,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 12.99,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Snacks",
|
|
||||||
datetime = now.minusDays(1),
|
|
||||||
categoryId = 2,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 210.00,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Hotel 5 nights",
|
|
||||||
datetime = now,
|
|
||||||
categoryId = 1,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 95.50,
|
|
||||||
currency = "EUR",
|
|
||||||
note = "Dinner for two",
|
|
||||||
datetime = now.minusHours(12),
|
|
||||||
categoryId = 2,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 30.00,
|
|
||||||
currency = "EUR",
|
|
||||||
note = "Taxi",
|
|
||||||
datetime = now.minusHours(6),
|
|
||||||
categoryId = 3,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 40.00,
|
|
||||||
currency = "USD",
|
|
||||||
note = "Gifts",
|
|
||||||
datetime = now.minusHours(3),
|
|
||||||
categoryId = 5,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
expenseDao.insert(
|
|
||||||
Expense(
|
|
||||||
amount = 75.00,
|
|
||||||
currency = "PLN",
|
|
||||||
note = "Sightseeing tour",
|
|
||||||
datetime = now.minusHours(1),
|
|
||||||
categoryId = 4,
|
|
||||||
tripId = 1
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
expense
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package cc.n0th1ng.tripmoney.data.dao
|
package cc.n0th1ng.tripmoney.data.dao
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
@@ -14,12 +15,23 @@ interface CategoryDao {
|
|||||||
suspend fun insert(category: Category)
|
suspend fun insert(category: Category)
|
||||||
|
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(category: Category)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM category
|
SELECT * FROM category WHERE archived is 0
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun categories(): Flow<List<Category>>
|
fun categories(): Flow<List<Category>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM category WHERE archived is 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun archivedCategories(): Flow<List<Category>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,22 +16,38 @@ interface ExpenseDao {
|
|||||||
@Upsert
|
@Upsert
|
||||||
suspend fun insert(expense: Expense)
|
suspend fun insert(expense: Expense)
|
||||||
|
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM expense WHERE trip_id = :tripId
|
SELECT expense.*, category.*
|
||||||
|
FROM expense
|
||||||
|
JOIN category ON expense.category_id = category.id
|
||||||
|
WHERE expense.trip_id = :tripId
|
||||||
|
AND
|
||||||
|
(
|
||||||
|
(:filter IS NULL OR category.name LIKE '%' || :filter || '%')
|
||||||
|
OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%')
|
||||||
|
)
|
||||||
ORDER BY expense.datetime DESC
|
ORDER BY expense.datetime DESC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun expenseDtoPaged(tripId: Int): PagingSource<Int, ExpenseDto>
|
fun expenseDtoPaged(tripId: Int, filter: String): PagingSource<Int, ExpenseDto>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM expense WHERE trip_id = :tripId
|
SELECT * FROM expense
|
||||||
|
JOIN category ON expense.category_id = category.id
|
||||||
|
WHERE trip_id = :tripId
|
||||||
|
AND
|
||||||
|
(
|
||||||
|
(:filter IS NULL OR category.name LIKE '%' || :filter || '%')
|
||||||
|
OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%')
|
||||||
|
)
|
||||||
ORDER BY expense.datetime DESC
|
ORDER BY expense.datetime DESC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun expenseDto(tripId: Int): Flow<List<ExpenseDto>>
|
fun expenseDto(tripId: Int, filter: String): Flow<List<ExpenseDto>>
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(expense: Expense)
|
suspend fun delete(expense: Expense)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface TripDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM trip
|
SELECT * FROM trip
|
||||||
ORDER BY DATE(trip.start_date) DESC
|
ORDER BY trip.start_date DESC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun tripsPaged(): PagingSource<Int, Trip>
|
fun tripsPaged(): PagingSource<Int, Trip>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
package cc.n0th1ng.tripmoney.data.entity
|
package cc.n0th1ng.tripmoney.data.entity
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import cc.n0th1ng.tripmoney.utils.Icons
|
import cc.n0th1ng.tripmoney.utils.Icons
|
||||||
|
|
||||||
@Entity(tableName = "category")
|
@Entity(tableName = "category")
|
||||||
|
@Immutable
|
||||||
data class Category(
|
data class Category(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo("name") val name: String,
|
@ColumnInfo("name") val name: String,
|
||||||
@ColumnInfo("icon") val icon: Icons,
|
@ColumnInfo("icon") val icon: Icons,
|
||||||
@ColumnInfo("color") val color: String
|
@ColumnInfo("color") val color: String,
|
||||||
|
@ColumnInfo("archived") val archived: Boolean = false
|
||||||
)
|
)
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
package cc.n0th1ng.tripmoney.data.entity
|
package cc.n0th1ng.tripmoney.data.entity
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Entity(tableName = "expense")
|
@Entity(
|
||||||
|
tableName = "expense",
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
entity = Category::class,
|
||||||
|
parentColumns = arrayOf("id"),
|
||||||
|
childColumns = arrayOf("category_id"),
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
@Immutable
|
||||||
data class Expense(
|
data class Expense(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo("amount") val amount: Double,
|
@ColumnInfo("amount") val amount: Double,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cc.n0th1ng.tripmoney.data.entity
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
@@ -9,14 +10,16 @@ import cc.n0th1ng.tripmoney.utils.Currencies
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
@Entity(tableName = "trip")
|
@Entity(tableName = "trip")
|
||||||
|
@Immutable
|
||||||
data class Trip(
|
data class Trip(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo("name") val name: String,
|
@ColumnInfo("name") val name: String,
|
||||||
@ColumnInfo("start_date") val startDate: LocalDate,
|
@ColumnInfo("start_date") val startDate: LocalDate,
|
||||||
@ColumnInfo("currency") val currency: String
|
@ColumnInfo("currency") val currency: String,
|
||||||
|
@ColumnInfo("budget") val budget: Double
|
||||||
){
|
){
|
||||||
companion object {
|
companion object {
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
val DUMMY = Trip(-1, "dummy", LocalDate.now(), Currencies.default().name)
|
val DUMMY = Trip(-1, "", LocalDate.now(), Currencies.default().name, budget = 0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,16 @@ class CategoryRepository @Inject constructor(private val categoryDao: CategoryDa
|
|||||||
categoryDao.insert(category)
|
categoryDao.insert(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun delete(category: Category) {
|
||||||
|
categoryDao.delete(category)
|
||||||
|
}
|
||||||
|
|
||||||
fun getCategories(): Flow<List<Category>> {
|
fun getCategories(): Flow<List<Category>> {
|
||||||
return categoryDao.categories()
|
return categoryDao.categories()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getArchivedCategories(): Flow<List<Category>> {
|
||||||
|
return categoryDao.archivedCategories()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,6 @@ class ExchangeRateRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
suspend fun clearOldRates(daysToKeep: Int = 180) {
|
suspend fun clearOldRates(daysToKeep: Int = 180) {
|
||||||
val cutoffDate = LocalDate.now().minusDays(daysToKeep.toLong()).toString()
|
val cutoffDate = LocalDate.now().minusDays(daysToKeep.toLong()).toString()
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw
|
|||||||
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.utils.Currencies
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -32,15 +35,15 @@ class ExpenseRepository @Inject constructor(
|
|||||||
expenseDao.delete(expense)
|
expenseDao.delete(expense)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpensesDtoPaged(tripId: Int): Flow<PagingData<ExpenseDto>> {
|
fun getExpensesDtoPaged(tripId: Int, filter: String): Flow<PagingData<ExpenseDto>> {
|
||||||
return Pager(
|
return Pager(
|
||||||
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
||||||
pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId) }
|
pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId, filter) }
|
||||||
).flow
|
).flow
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpensesDto(tripId: Int): Flow<List<ExpenseDto>> {
|
fun getExpensesDto(tripId: Int, filter: String = ""): Flow<List<ExpenseDto>> {
|
||||||
return expenseDao.expenseDto(tripId)
|
return expenseDao.expenseDto(tripId, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import androidx.paging.PagingData
|
|||||||
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TripRepository @Inject constructor(
|
class TripRepository @Inject constructor(
|
||||||
@@ -18,9 +21,7 @@ class TripRepository @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@WorkerThread
|
|
||||||
suspend fun save(trip: Trip) {
|
suspend fun save(trip: Trip) {
|
||||||
expenseRepository.recalculateTripExpenses(trip.id)
|
|
||||||
tripDao.insert(trip)
|
tripDao.insert(trip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import androidx.compose.material3.NavigationBar
|
|||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
@@ -37,7 +39,7 @@ fun BottomNavigation(navController: NavController) {
|
|||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
R.drawable.materialsymbols_ic_list_outlined,
|
R.drawable.materialsymbols_ic_list_outlined,
|
||||||
),
|
),
|
||||||
null
|
"list screen"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,4 +100,5 @@ object Screens {
|
|||||||
const val TRIP_PICKER = "trip_picker"
|
const val TRIP_PICKER = "trip_picker"
|
||||||
const val STATISTICS = "statistics"
|
const val STATISTICS = "statistics"
|
||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
|
const val MANAGE_CATEGORIES = "manage_categories"
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,130 @@
|
|||||||
package cc.n0th1ng.tripmoney.navigation
|
package cc.n0th1ng.tripmoney.navigation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
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.Close
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldColors
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
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.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import cc.n0th1ng.tripmoney.R
|
import cc.n0th1ng.tripmoney.R
|
||||||
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
|
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||||
|
import com.composables.icons.materialsymbols.outlined.R.drawable
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBar(onClick: () -> Unit, title: String = "") {
|
fun TopBar(
|
||||||
|
onDrawerClick: () -> Unit,
|
||||||
|
title: String = "",
|
||||||
|
isSearchable: Boolean = false,
|
||||||
|
onFilterChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var isSearch by remember { mutableStateOf(false) }
|
||||||
|
var value by remember { mutableStateOf("") }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(title) },
|
title = {
|
||||||
|
if (isSearch && isSearchable) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier.fillMaxWidth(0.9f).focusRequester(focusRequester),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
errorContainerColor = Color.Transparent
|
||||||
|
),
|
||||||
|
value = value,
|
||||||
|
onValueChange = { newText ->
|
||||||
|
value = newText
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.clickable(onClick = {
|
||||||
|
isSearch = false
|
||||||
|
value = ""
|
||||||
|
onFilterChange("")
|
||||||
|
}),
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
LaunchedEffect(key1 = value) {
|
||||||
|
delay(1000)
|
||||||
|
onFilterChange(value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(title)
|
||||||
|
}
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onClick() }) {
|
IconButton(onClick = { onDrawerClick() }) {
|
||||||
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (!isSearch && isSearchable) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(end = 13.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(15.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
painter = painterResource(drawable.materialsymbols_ic_filter_alt_outlined),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.clickable(onClick = {})
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.clickable(onClick = {
|
||||||
|
isSearch = true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -40,3 +143,15 @@ fun TopBarSettings(navController: NavHostController) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewTopBar() {
|
||||||
|
TripMoneyTheme {
|
||||||
|
TopBar(
|
||||||
|
onDrawerClick = {},
|
||||||
|
title = "Essa",
|
||||||
|
onFilterChange = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package cc.n0th1ng.tripmoney.screens
|
package cc.n0th1ng.tripmoney.screens
|
||||||
|
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
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.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
@@ -8,14 +11,17 @@ 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.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -27,44 +33,68 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
|
import cc.n0th1ng.tripmoney.R
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
|
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||||
import cc.n0th1ng.tripmoney.utils.Icons
|
import cc.n0th1ng.tripmoney.utils.Icons
|
||||||
import cc.n0th1ng.tripmoney.utils.colors
|
import cc.n0th1ng.tripmoney.utils.colors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
|
fun AddCategoryDialog(
|
||||||
var name by remember { mutableStateOf("") }
|
onDismiss: () -> Unit,
|
||||||
var icon by remember { mutableStateOf(Icons.entries[0]) }
|
onSave: (Category) -> Unit,
|
||||||
var color by remember { mutableStateOf(colors[0]) }
|
categoryToEdit: Category? = null
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf(categoryToEdit?.name ?: "") }
|
||||||
|
var icon by remember { mutableStateOf(categoryToEdit?.icon ?: Icons.entries[0]) }
|
||||||
|
var color by remember { mutableStateOf(categoryToEdit?.color ?: colors[0]) }
|
||||||
|
var isArchived by remember { mutableStateOf(categoryToEdit?.archived ?: false) }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss, title = { Text("Add new category") }, text = {
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(if (categoryToEdit == null) R.string.add_new_category else R.string.edit_category)) },
|
||||||
|
text = {
|
||||||
AlertDialogFill(
|
AlertDialogFill(
|
||||||
onTextChange = { newText ->
|
onTextChange = { newText ->
|
||||||
name = newText
|
name = newText
|
||||||
},
|
},
|
||||||
onIconChange = { newIcon -> icon = newIcon },
|
onIconChange = { newIcon -> icon = newIcon },
|
||||||
onColorChange = { newColor -> color = newColor }
|
onColorChange = { newColor -> color = newColor },
|
||||||
|
onArchivedChange = { newArchived ->
|
||||||
|
isArchived = newArchived
|
||||||
|
},
|
||||||
|
name = name,
|
||||||
|
icon = icon,
|
||||||
|
color = color,
|
||||||
|
isArchived = isArchived
|
||||||
)
|
)
|
||||||
}, confirmButton = {
|
},
|
||||||
|
confirmButton = {
|
||||||
Button(
|
Button(
|
||||||
enabled = !name.isEmpty(),
|
enabled = !name.isEmpty(),
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(
|
val categoryToSave = Category(
|
||||||
Category(
|
|
||||||
name = name,
|
name = name,
|
||||||
icon = icon,
|
icon = icon,
|
||||||
color = color
|
color = color,
|
||||||
|
archived = isArchived
|
||||||
)
|
)
|
||||||
|
onSave(
|
||||||
|
if (categoryToEdit != null) categoryToSave.copy(id = categoryToEdit.id) else categoryToSave
|
||||||
)
|
)
|
||||||
}) { Text("Save") }
|
}) { Text(stringResource(R.string.save)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
Row() {
|
||||||
Button(
|
Button(
|
||||||
colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.error),
|
colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.secondary),
|
||||||
onClick = onDismiss
|
onClick = onDismiss
|
||||||
) { Text("close") }
|
) { Text(stringResource(R.string.cancel)) }
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,24 +102,26 @@ fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
|
|||||||
fun AlertDialogFill(
|
fun AlertDialogFill(
|
||||||
onTextChange: (String) -> Unit,
|
onTextChange: (String) -> Unit,
|
||||||
onIconChange: (Icons) -> Unit,
|
onIconChange: (Icons) -> Unit,
|
||||||
onColorChange: (String) -> Unit
|
onColorChange: (String) -> Unit,
|
||||||
|
onArchivedChange: (Boolean) -> Unit,
|
||||||
|
name: String,
|
||||||
|
icon: Icons,
|
||||||
|
color: String,
|
||||||
|
isArchived: Boolean
|
||||||
) {
|
) {
|
||||||
var text by remember { mutableStateOf("") }
|
|
||||||
var iconId by remember { mutableIntStateOf(Icons.entries[0].resource) }
|
|
||||||
var colorHex by remember { mutableStateOf(colors[0]) }
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier.size(30.dp),
|
||||||
painter = painterResource(iconId), contentDescription = null,
|
painter = painterResource(icon.resource), contentDescription = null,
|
||||||
tint = Color(colorHex.toColorInt())
|
tint = Color(color.toColorInt())
|
||||||
)
|
)
|
||||||
OutlinedTextField(label = { Text("Name") }, value = text, onValueChange = { newText ->
|
OutlinedTextField(label = { Text("Name") }, value = name, onValueChange = { newText ->
|
||||||
text = newText
|
onTextChange(newText)
|
||||||
onTextChange(text)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +136,6 @@ fun AlertDialogFill(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
.clickable(onClick = {
|
.clickable(onClick = {
|
||||||
iconId = icon.resource
|
|
||||||
onIconChange(icon)
|
onIconChange(icon)
|
||||||
}),
|
}),
|
||||||
painter = painterResource(icon.resource),
|
painter = painterResource(icon.resource),
|
||||||
@@ -123,8 +154,7 @@ fun AlertDialogFill(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = {
|
.clickable(onClick = {
|
||||||
colorHex = color
|
onColorChange(color)
|
||||||
onColorChange(colorHex)
|
|
||||||
})
|
})
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
@@ -132,5 +162,49 @@ fun AlertDialogFill(
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Switch(
|
||||||
|
checked = isArchived,
|
||||||
|
onCheckedChange = onArchivedChange
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Archived",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewAddCategoryDialog() {
|
||||||
|
TripMoneyTheme {
|
||||||
|
AddCategoryDialog(
|
||||||
|
onDismiss = {},
|
||||||
|
onSave = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewEditCategoryDialog() {
|
||||||
|
TripMoneyTheme {
|
||||||
|
AddCategoryDialog(
|
||||||
|
onDismiss = {},
|
||||||
|
onSave = {},
|
||||||
|
categoryToEdit = Category(
|
||||||
|
0, "Hotel",
|
||||||
|
icon = cc.n0th1ng.tripmoney.utils.Icons.entries.random(),
|
||||||
|
color = colors.random(),
|
||||||
|
archived = true
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -11,6 +12,7 @@ import androidx.compose.foundation.interaction.PressInteraction
|
|||||||
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
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -224,7 +226,7 @@ fun AddExpenseBottomSheet(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
|
text = datetime.format(DateTimeFormatter.ofPattern("dd.MM HH:mm")),
|
||||||
fontSize = 17.sp
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CategoryButton(
|
CategoryButton(
|
||||||
@@ -265,6 +267,11 @@ fun AddExpenseBottomSheet(
|
|||||||
equationResult = evaluate(amount)
|
equationResult = evaluate(amount)
|
||||||
enableSave = amount.isDoubleTwoDigitsOrEquation() && equationResult > 0
|
enableSave = amount.isDoubleTwoDigitsOrEquation() && equationResult > 0
|
||||||
},
|
},
|
||||||
|
onLongBackspaceClick = {
|
||||||
|
amount = "0.00"
|
||||||
|
equationResult = evaluate(amount)
|
||||||
|
enableSave = false
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SaveButton(
|
SaveButton(
|
||||||
@@ -394,7 +401,13 @@ fun NoteInput(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) {
|
fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) {
|
||||||
Button(onClick = onClick, modifier = modifier, shape = MaterialTheme.shapes.medium) {
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = ButtonDefaults.buttonColors()
|
||||||
|
.copy(containerColor = MaterialTheme.colorScheme.secondary)
|
||||||
|
) {
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,18 +415,35 @@ fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: Str
|
|||||||
@Composable
|
@Composable
|
||||||
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) {
|
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) {
|
||||||
Button(
|
Button(
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
colors = ButtonDefaults.buttonColors()
|
colors = ButtonDefaults.buttonColors()
|
||||||
.copy(containerColor = Color(category.color.toColorInt()), contentColor = Color.Black)
|
.copy(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
|
// Row(modifier = modifier.fillMaxWidth()) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
tint = Color(category.color.toColorInt()),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
|
// .background(
|
||||||
|
// color = MaterialTheme.colorScheme.prima,
|
||||||
|
// shape = MaterialTheme.shapes.small
|
||||||
|
// )
|
||||||
|
.padding(end = 10.dp),
|
||||||
painter = painterResource(category.icon.resource),
|
painter = painterResource(category.icon.resource),
|
||||||
contentDescription = stringResource(R.string.category),
|
contentDescription = stringResource(R.string.category),
|
||||||
)
|
)
|
||||||
Text(category.name)
|
Text(
|
||||||
|
text = category.name,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +467,8 @@ fun NumberKeyboard(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onNumberClick: (String) -> Unit,
|
onNumberClick: (String) -> Unit,
|
||||||
onBackspaceClick: () -> Unit,
|
onBackspaceClick: () -> Unit,
|
||||||
onOperatorClick: (String) -> Unit
|
onOperatorClick: (String) -> Unit,
|
||||||
|
onLongBackspaceClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -455,22 +486,23 @@ fun NumberKeyboard(
|
|||||||
onClick = onBackspaceClick,
|
onClick = onBackspaceClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
containerColor = Color.Transparent,
|
||||||
|
onLongClick = onLongBackspaceClick
|
||||||
)
|
)
|
||||||
|
|
||||||
"+", "/", "-", "*" -> KeyboardButton(
|
"+", "/", "-", "*" -> KeyboardButton(
|
||||||
text = key,
|
text = key,
|
||||||
onClick = { onOperatorClick(key) },
|
onClick = { onOperatorClick(key) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> KeyboardButton(
|
else -> KeyboardButton(
|
||||||
text = key,
|
text = key,
|
||||||
onClick = { onNumberClick(key) },
|
onClick = { onNumberClick(key) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
containerColor = MaterialTheme.colorScheme.secondary,
|
containerColor = Color.Transparent,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondary
|
contentColor = MaterialTheme.colorScheme.onSecondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -487,26 +519,24 @@ fun KeyboardButton(
|
|||||||
icon: Painter? = null,
|
icon: Painter? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
onLongClick: () -> Unit = {},
|
||||||
containerColor: Color = MaterialTheme.colorScheme.primary,
|
containerColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
contentColor: Color = MaterialTheme.colorScheme.onPrimary
|
contentColor: Color = MaterialTheme.colorScheme.onPrimary
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Button(
|
Box(
|
||||||
onClick = onClick,
|
contentAlignment = Alignment.Center,
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
.aspectRatio(2.5f),
|
.aspectRatio(2.5f)
|
||||||
enabled = enabled,
|
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||||
colors = ButtonDefaults.buttonColors(
|
.background(containerColor, shape = MaterialTheme.shapes.medium),
|
||||||
containerColor = containerColor,
|
|
||||||
contentColor = contentColor
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
text != null -> Text(
|
text != null -> Text(
|
||||||
text,
|
text,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.headlineMedium
|
||||||
)
|
)
|
||||||
|
|
||||||
icon != null -> Icon(painter = icon, contentDescription = null)
|
icon != null -> Icon(painter = icon, contentDescription = null)
|
||||||
@@ -594,26 +624,31 @@ fun PreviewAddExpenseEnabled() {
|
|||||||
|
|
||||||
val categoriesToPreview = listOf(
|
val categoriesToPreview = listOf(
|
||||||
Category(
|
Category(
|
||||||
|
1,
|
||||||
name = "Hotel",
|
name = "Hotel",
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
||||||
color = colors.random()
|
color = colors.random()
|
||||||
),
|
),
|
||||||
Category(
|
Category(
|
||||||
|
2,
|
||||||
name = "Jedzenie",
|
name = "Jedzenie",
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
||||||
color = colors.random()
|
color = colors.random()
|
||||||
),
|
),
|
||||||
Category(
|
Category(
|
||||||
|
3,
|
||||||
name = "Transport",
|
name = "Transport",
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
||||||
color = colors.random()
|
color = colors.random()
|
||||||
),
|
),
|
||||||
Category(
|
Category(
|
||||||
|
4,
|
||||||
name = "Rozrywka",
|
name = "Rozrywka",
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
||||||
color = colors.random()
|
color = colors.random()
|
||||||
),
|
),
|
||||||
Category(
|
Category(
|
||||||
|
5,
|
||||||
name = "Zakupy",
|
name = "Zakupy",
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
color = colors.random()
|
color = colors.random()
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import cc.n0th1ng.tripmoney.R.*
|
import cc.n0th1ng.tripmoney.R.*
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
|
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
|
||||||
import cc.n0th1ng.tripmoney.utils.Icons
|
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
import com.composables.icons.materialsymbols.outlined.R
|
import com.composables.icons.materialsymbols.outlined.R
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ fun CategorySelectionDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onCategorySelected: (Category) -> Unit,
|
onCategorySelected: (Category) -> Unit,
|
||||||
selected: Category,
|
selected: Category,
|
||||||
categories: List<Category>
|
categories: List<Category>,
|
||||||
) {
|
) {
|
||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -91,7 +90,7 @@ fun CategorySelectionDialog(
|
|||||||
contentDescription = stringResource(string.category)
|
contentDescription = stringResource(string.category)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(string.add_new_category), modifier = Modifier.padding(start = 8.dp),
|
text = stringResource(string.add_new), modifier = Modifier.padding(start = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ 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.BasicAlertDialog
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
@@ -47,6 +46,7 @@ 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.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -58,33 +58,46 @@ import androidx.paging.PagingData
|
|||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
import androidx.paging.compose.itemKey
|
||||||
import cc.n0th1ng.tripmoney.R.string
|
import cc.n0th1ng.tripmoney.R.string
|
||||||
|
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.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
||||||
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
|
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
|
import cc.n0th1ng.tripmoney.utils.colors
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel.ExpenseListItemUi
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel.ExpenseListItemUi
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun ListExpenseScreen() {
|
fun ListExpenseScreen(filter: String) {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val expensesFlow = expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId)
|
val expensesFlow =
|
||||||
|
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, filter)
|
||||||
|
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
|
||||||
|
|
||||||
ListExpenseScreen(
|
ListExpenseScreen(
|
||||||
expensesFlow = expensesFlow,
|
expensesFlow = expensesFlow,
|
||||||
onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) },
|
onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) },
|
||||||
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) },
|
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) },
|
||||||
|
isRecalculatingRate = isRecalculatingRate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +107,8 @@ fun ListExpenseScreen() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ListExpenseScreen(
|
fun ListExpenseScreen(
|
||||||
expensesFlow: Flow<PagingData<ExpenseListItemUi>>,
|
expensesFlow: Flow<PagingData<ExpenseListItemUi>>,
|
||||||
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit
|
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit,
|
||||||
|
isRecalculatingRate: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val items = expensesFlow.collectAsLazyPagingItems()
|
val items = expensesFlow.collectAsLazyPagingItems()
|
||||||
@@ -111,13 +125,7 @@ fun ListExpenseScreen(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
if (items.loadState.refresh == LoadState.Loading) {
|
Box {
|
||||||
// Show loading indicator
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -208,7 +216,7 @@ fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
|
|||||||
date.format(
|
date.format(
|
||||||
DateTimeFormatter.ofPattern("dd EEEE")
|
DateTimeFormatter.ofPattern("dd EEEE")
|
||||||
).toString(),
|
).toString(),
|
||||||
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
modifier = Modifier.padding(horizontal = 5.dp).background(Color.White.copy(alpha = 0f)),
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
@@ -221,8 +229,14 @@ fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
|
|||||||
HorizontalDivider(modifier = Modifier.weight(2f))
|
HorizontalDivider(modifier = Modifier.weight(2f))
|
||||||
Text(
|
Text(
|
||||||
"%.2f %s".format(sum, currency),
|
"%.2f %s".format(sum, currency),
|
||||||
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
modifier = Modifier
|
||||||
style = MaterialTheme.typography.bodyMedium
|
.background(
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(5.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
@@ -256,7 +270,7 @@ fun SwipeToDeleteExpenseCard(
|
|||||||
Modifier
|
Modifier
|
||||||
.clip(CardDefaults.elevatedShape)
|
.clip(CardDefaults.elevatedShape)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.onError)
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
.padding(horizontal = 20.dp),
|
.padding(horizontal = 20.dp),
|
||||||
contentAlignment = Alignment.CenterEnd
|
contentAlignment = Alignment.CenterEnd
|
||||||
) {
|
) {
|
||||||
@@ -329,7 +343,7 @@ fun ExpenseCard(
|
|||||||
) {
|
) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = CardDefaults.elevatedCardColors()
|
colors = CardDefaults.elevatedCardColors()
|
||||||
.copy(containerColor = MaterialTheme.colorScheme.secondaryContainer),
|
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.height(70.dp)
|
.height(70.dp)
|
||||||
@@ -344,14 +358,29 @@ fun ExpenseCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
//TODO
|
||||||
|
// .background(
|
||||||
|
// Brush.horizontalGradient(
|
||||||
|
// colorStops = arrayOf(
|
||||||
|
// 1f to Color(expenseDto.category.color.toColorInt()),
|
||||||
|
// 4f to MaterialTheme.colorScheme.surfaceDim
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// )
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(15.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
modifier = Modifier.fillMaxHeight()
|
modifier = Modifier.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceDim,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(10.dp),
|
||||||
painter = painterResource(expenseDto.category.icon.resource),
|
painter = painterResource(expenseDto.category.icon.resource),
|
||||||
contentDescription = "Category",
|
contentDescription = "Category",
|
||||||
tint = Color(expenseDto.category.color.toColorInt())
|
tint = Color(expenseDto.category.color.toColorInt())
|
||||||
@@ -367,13 +396,13 @@ fun ExpenseCard(
|
|||||||
Text(
|
Text(
|
||||||
text = expenseDto.category.name,
|
text = expenseDto.category.name,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(0.dp),
|
modifier = Modifier.padding(0.dp),
|
||||||
text = expenseDto.expense.note,
|
text = expenseDto.expense.note,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +411,7 @@ fun ExpenseCard(
|
|||||||
DateTimeFormatter.ofPattern("dd MMM HH:mm")
|
DateTimeFormatter.ofPattern("dd MMM HH:mm")
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +419,7 @@ fun ExpenseCard(
|
|||||||
Text(
|
Text(
|
||||||
text = "- %.2f ${expenseDto.expense.currency}".format(expenseDto.expense.amount),
|
text = "- %.2f ${expenseDto.expense.currency}".format(expenseDto.expense.amount),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -398,7 +427,7 @@ fun ExpenseCard(
|
|||||||
Text(
|
Text(
|
||||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.convertedAmount()),
|
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.convertedAmount()),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,107 +436,125 @@ fun ExpenseCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
//@AllPreviews
|
@AllPreviews
|
||||||
//@Composable
|
@Composable
|
||||||
//fun PreviewListExpenseScreen() {
|
fun PreviewListExpenseScreen() {
|
||||||
// TripMoneyTheme() {
|
TripMoneyTheme() {
|
||||||
// val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
|
val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
|
||||||
// ListExpenseScreen(
|
ListExpenseScreen(
|
||||||
// expensesDtoFlow = MutableStateFlow(pagingData),
|
expensesFlow = MutableStateFlow(pagingData),
|
||||||
// onSaveExpense = {},
|
onSaveExpense = {},
|
||||||
// onDeleteExpense = {},
|
onDeleteExpense = {},
|
||||||
// dailySums = emptyMap()
|
true
|
||||||
// )
|
)
|
||||||
//
|
|
||||||
// }
|
}
|
||||||
//}
|
}
|
||||||
//
|
|
||||||
//@AllPreviews
|
@AllPreviews
|
||||||
//@Composable
|
@Composable
|
||||||
//fun PreviewDeleteConfirmationDialog() {
|
fun PreviewDeleteConfirmationDialog() {
|
||||||
// TripMoneyTheme() {
|
TripMoneyTheme() {
|
||||||
// DeleteConfirmationDialog(
|
DeleteConfirmationDialog(
|
||||||
// onConfirm = {},
|
onConfirm = {},
|
||||||
// onCancel = {})
|
onCancel = {})
|
||||||
// }
|
}
|
||||||
//}
|
}
|
||||||
//
|
|
||||||
//
|
|
||||||
//@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
//private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseDto> {
|
private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseListItemUi> {
|
||||||
// val sampleCategories = listOf(
|
val sampleCategories = listOf(
|
||||||
// Category(
|
Category(
|
||||||
// name = "Hotel",
|
name = "Hotel",
|
||||||
// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
||||||
// color = colors.random()
|
color = colors.random()
|
||||||
// ),
|
),
|
||||||
// Category(
|
Category(
|
||||||
// name = "Jedzenie",
|
name = "Jedzenie",
|
||||||
// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
||||||
// color = colors.random()
|
color = colors.random()
|
||||||
// ),
|
),
|
||||||
// Category(
|
Category(
|
||||||
// name = "Transport",
|
name = "Transport",
|
||||||
// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
||||||
// color = colors.random()
|
color = colors.random()
|
||||||
// ),
|
),
|
||||||
// Category(
|
Category(
|
||||||
// name = "Rozrywka",
|
name = "Rozrywka",
|
||||||
// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
||||||
// color = colors.random()
|
color = colors.random()
|
||||||
// ),
|
),
|
||||||
// Category(
|
Category(
|
||||||
// name = "Zakupy",
|
name = "Zakupy",
|
||||||
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
// color = colors.random()
|
color = colors.random()
|
||||||
// ),
|
),
|
||||||
// )
|
)
|
||||||
//
|
|
||||||
// val trip = Trip(
|
val trip = Trip(
|
||||||
// id = 1,
|
id = 1,
|
||||||
// name = "Vacation",
|
name = "Vacation",
|
||||||
// currency = "USD",
|
currency = "USD",
|
||||||
// startDate = LocalDate.parse("2026-01-01")
|
startDate = LocalDate.parse("2026-01-01")
|
||||||
// )
|
)
|
||||||
//
|
|
||||||
// val startLong = LocalDateTime.now().minusDays(10).toEpochMilli()
|
val startLong = LocalDateTime.now().minusDays(10).toEpochMilli()
|
||||||
// val endLong = LocalDateTime.now().toEpochMilli()
|
val endLong = LocalDateTime.now().toEpochMilli()
|
||||||
//
|
|
||||||
// val result: MutableList<ExpenseDto> = mutableListOf()
|
val result: MutableList<ExpenseListItemUi> = mutableListOf()
|
||||||
// for (i in 0..15) {
|
result.add(
|
||||||
// val category = sampleCategories.random()
|
ExpenseListItemUi.Header(
|
||||||
// val datetime = if (i > 4) {
|
LocalDateTime.ofEpochSecond(
|
||||||
// LocalDateTime.ofEpochSecond(
|
Random.nextLong(startLong, endLong),
|
||||||
// Random.nextLong(startLong, endLong),
|
0,
|
||||||
// 0,
|
ZoneOffset.UTC
|
||||||
// ZoneOffset.UTC
|
).toLocalDate(), Random.nextDouble(0.1, 300.0), Currencies.entries.random().name
|
||||||
// )
|
)
|
||||||
// } else LocalDateTime.now()
|
)
|
||||||
//
|
for (i in 0..15) {
|
||||||
// val expense = Expense(
|
val category = sampleCategories.random()
|
||||||
// id = i,
|
val datetime = if (i > 4) {
|
||||||
// categoryId = category.id,
|
LocalDateTime.ofEpochSecond(
|
||||||
// tripId = 1,
|
Random.nextLong(startLong, endLong),
|
||||||
// amount = Random.nextDouble(0.1, 300.0),
|
0,
|
||||||
// currency = Currencies.entries.random().name,
|
ZoneOffset.UTC
|
||||||
// note = if (i % 3 == 0) "Some note" else "",
|
)
|
||||||
// datetime = datetime,
|
} else LocalDateTime.now()
|
||||||
// rate = if (Random.nextBoolean()) Random.nextDouble(
|
|
||||||
// 0.1,
|
val expense = Expense(
|
||||||
// 5.0
|
id = i,
|
||||||
// ) else 1.0
|
categoryId = category.id,
|
||||||
// )
|
tripId = 1,
|
||||||
//
|
amount = Random.nextDouble(0.1, 300.0),
|
||||||
//
|
currency = Currencies.entries.random().name,
|
||||||
// val expenseDto = ExpenseDto(
|
note = if (i % 3 == 0) "Some note" else "",
|
||||||
// expense = expense,
|
datetime = datetime,
|
||||||
// category = category,
|
rate = if (Random.nextBoolean()) Random.nextDouble(
|
||||||
// trip = trip
|
0.1,
|
||||||
// )
|
5.0
|
||||||
// result.add(
|
) else 1.0
|
||||||
// expenseDto
|
)
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// return result
|
val expenseDto = ExpenseDto(
|
||||||
//}
|
expense = expense,
|
||||||
|
category = category,
|
||||||
|
trip = trip
|
||||||
|
)
|
||||||
|
result.add(
|
||||||
|
ExpenseListItemUi.Item(expenseDto)
|
||||||
|
)
|
||||||
|
if (i % 5 == 0) {
|
||||||
|
result.add(
|
||||||
|
ExpenseListItemUi.Header(
|
||||||
|
datetime.toLocalDate(),
|
||||||
|
Random.nextDouble(0.1, 300.0),
|
||||||
|
Currencies.entries.random().name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
package cc.n0th1ng.tripmoney.screens.managecategories
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
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
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
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.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SwipeToDismissBox
|
||||||
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import cc.n0th1ng.tripmoney.R.string
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
|
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
|
||||||
|
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
|
||||||
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
|
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||||
|
import cc.n0th1ng.tripmoney.utils.colors
|
||||||
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
|
import com.composables.icons.materialsymbols.outlined.R
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.collections.emptyList
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Composable
|
||||||
|
fun ManageCategoriesScreen() {
|
||||||
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
|
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||||
|
val archivedCategories by expenseAndCategoryViewModel.getArchivedCategories()
|
||||||
|
.collectAsState(emptyList())
|
||||||
|
ManageCategoriesScreen(
|
||||||
|
categories = categories,
|
||||||
|
archivedCategories = archivedCategories,
|
||||||
|
onSaveCategory = { expenseAndCategoryViewModel.save(it) },
|
||||||
|
onDeleteCategory = {
|
||||||
|
expenseAndCategoryViewModel.delete(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Composable
|
||||||
|
fun ManageCategoriesScreen(
|
||||||
|
categories: List<Category>,
|
||||||
|
archivedCategories: List<Category>,
|
||||||
|
onSaveCategory: (Category) -> Unit,
|
||||||
|
onDeleteCategory: (Category) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var categoryToEdit by remember { mutableStateOf<Category?>(null) }
|
||||||
|
var showAddCategoryDialog by remember { mutableStateOf(false) }
|
||||||
|
var itemToDelete by remember { mutableStateOf<Category?>(null) }
|
||||||
|
var itemToArchive by remember { mutableStateOf<Category?>(null) }
|
||||||
|
|
||||||
|
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = { showAddCategoryDialog = true },
|
||||||
|
icon = { Icon(Icons.Filled.Add, stringResource(string.add_new)) },
|
||||||
|
text = { Text(text = stringResource(string.add_new)) },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
items(categories, key = { it.id }) { category ->
|
||||||
|
SwipeToDeleteExpenseCard(
|
||||||
|
category = category,
|
||||||
|
onDelete = { itemToArchive = category },
|
||||||
|
onClick = {
|
||||||
|
categoryToEdit = category
|
||||||
|
showAddCategoryDialog = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (archivedCategories.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
CustomDivider()
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(archivedCategories, key = { it.id }) { archivedCategory ->
|
||||||
|
SwipeToDeleteExpenseCard(
|
||||||
|
category = archivedCategory,
|
||||||
|
onDelete = { itemToDelete = archivedCategory },
|
||||||
|
onClick = {
|
||||||
|
categoryToEdit = archivedCategory
|
||||||
|
showAddCategoryDialog = true
|
||||||
|
},
|
||||||
|
isArchived = true
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (showAddCategoryDialog) {
|
||||||
|
AddCategoryDialog(
|
||||||
|
onDismiss = {
|
||||||
|
showAddCategoryDialog = false
|
||||||
|
}, onSave = { category ->
|
||||||
|
onSaveCategory(category)
|
||||||
|
showAddCategoryDialog = false
|
||||||
|
},
|
||||||
|
categoryToEdit = categoryToEdit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemToDelete != null) {
|
||||||
|
DeleteConfirmationDialog(
|
||||||
|
bodyText = stringResource(string.delete_category_info),
|
||||||
|
onConfirm = {
|
||||||
|
onDeleteCategory(itemToDelete!!)
|
||||||
|
itemToDelete = null
|
||||||
|
},
|
||||||
|
onCancel = {
|
||||||
|
itemToDelete = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemToArchive != null) {
|
||||||
|
DeleteConfirmationDialog(
|
||||||
|
title = stringResource(string.you_want_archive),
|
||||||
|
buttonText = stringResource(string.archive),
|
||||||
|
bodyText = stringResource(string.archive_category_info),
|
||||||
|
onConfirm = {
|
||||||
|
onSaveCategory(itemToArchive!!.copy(archived = true))
|
||||||
|
itemToArchive = null
|
||||||
|
},
|
||||||
|
onCancel = {
|
||||||
|
itemToArchive = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Composable
|
||||||
|
private fun CustomDivider() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Absolute.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
"Archived",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
.background(Color.White.copy(alpha = 0f)),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Composable
|
||||||
|
fun SwipeToDeleteExpenseCard(
|
||||||
|
category: Category,
|
||||||
|
onDelete: (Category) -> Unit,
|
||||||
|
onClick: (Category) -> Unit,
|
||||||
|
isArchived: Boolean = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
|
confirmValueChange = { dismissValue ->
|
||||||
|
if (dismissValue == SwipeToDismissBoxValue.EndToStart) {
|
||||||
|
onDelete(category)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwipeToDismissBox(
|
||||||
|
state = dismissState,
|
||||||
|
enableDismissFromStartToEnd = false,
|
||||||
|
backgroundContent = {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.clip(CardDefaults.elevatedShape)
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.onError)
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
if (isArchived) R.drawable.materialsymbols_ic_delete_outlined
|
||||||
|
else R.drawable.materialsymbols_ic_archive_outlined
|
||||||
|
),
|
||||||
|
contentDescription = stringResource(string.delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CategoryCard(
|
||||||
|
category = category,
|
||||||
|
onClick = onClick,
|
||||||
|
isArchived = isArchived
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DeleteConfirmationDialog(
|
||||||
|
title: String = stringResource(string.delete_confirmation),
|
||||||
|
buttonText: String = stringResource(string.delete),
|
||||||
|
bodyText: String = "",
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = { onCancel() }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
Text(
|
||||||
|
text = bodyText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 24.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onCancel,
|
||||||
|
colors = ButtonDefaults.buttonColors().copy(containerColor = MaterialTheme.colorScheme.secondary),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
){
|
||||||
|
Text(text = stringResource(string.cancel),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondary)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = onConfirm,
|
||||||
|
colors = ButtonDefaults.buttonColors().copy(containerColor = MaterialTheme.colorScheme.error),
|
||||||
|
){
|
||||||
|
Text(text = buttonText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Composable
|
||||||
|
fun CategoryCard(
|
||||||
|
category: Category,
|
||||||
|
onClick: (Category) -> Unit,
|
||||||
|
isArchived: Boolean = false
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = CardDefaults.elevatedCardColors()
|
||||||
|
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.height(70.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
enabled = true,
|
||||||
|
onClick = { onClick(category) },
|
||||||
|
onLongClick = { onClick(category) }),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.alpha(if (isArchived) 0.6f else 1f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(15.dp),
|
||||||
|
modifier = Modifier.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceDim,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(10.dp),
|
||||||
|
painter = painterResource(category.icon.resource),
|
||||||
|
contentDescription = "Category",
|
||||||
|
tint = Color(category.color.toColorInt())
|
||||||
|
)
|
||||||
|
|
||||||
|
Column()
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = category.name,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewManageCategoriesScreen() {
|
||||||
|
TripMoneyTheme {
|
||||||
|
ManageCategoriesScreen(categories = categoriesToPreview.subList(0,2), categoriesToPreview.subList(3,5), {}, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewAddCategoryDialog() {
|
||||||
|
TripMoneyTheme {
|
||||||
|
AddCategoryDialog(
|
||||||
|
onDismiss = {},
|
||||||
|
onSave = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewEditCategoryDialog() {
|
||||||
|
TripMoneyTheme {
|
||||||
|
AddCategoryDialog(
|
||||||
|
onDismiss = {},
|
||||||
|
onSave = {},
|
||||||
|
categoryToEdit = Category(
|
||||||
|
0, "Hotel",
|
||||||
|
icon = cc.n0th1ng.tripmoney.utils.Icons.entries.random(),
|
||||||
|
color = colors.random(),
|
||||||
|
archived = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewDeleteConfirmationDialog() {
|
||||||
|
TripMoneyTheme {
|
||||||
|
DeleteConfirmationDialog(
|
||||||
|
onConfirm = {},
|
||||||
|
onCancel = {},
|
||||||
|
bodyText = "Your all expenses with category Hotel will be removed.",
|
||||||
|
title = "Do you want to delete?",
|
||||||
|
buttonText = "Delete"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -38,10 +38,16 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import cc.n0th1ng.tripmoney.R.*
|
import cc.n0th1ng.tripmoney.R.*
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
||||||
|
import cc.n0th1ng.tripmoney.navigation.Screens
|
||||||
|
import cc.n0th1ng.tripmoney.screens.listexpense.CategorySelectionDialog
|
||||||
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
||||||
|
import cc.n0th1ng.tripmoney.screens.statistics.categories
|
||||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
@@ -60,7 +66,7 @@ import java.nio.file.Files
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.S)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen() {
|
fun SettingsScreen(navController: NavHostController) {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val currentTheme by settingsViewModel.theme.collectAsState()
|
val currentTheme by settingsViewModel.theme.collectAsState()
|
||||||
val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
|
val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
|
||||||
@@ -68,6 +74,7 @@ fun SettingsScreen() {
|
|||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
|
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val tripName = currentTrip?.name ?: ""
|
val tripName = currentTrip?.name ?: ""
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -90,7 +97,8 @@ fun SettingsScreen() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
onCategoriesClick = {navController.navigate(Screens.MANAGE_CATEGORIES)}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +111,13 @@ fun SettingsScreen(
|
|||||||
onCurrencySave: (Currencies) -> Unit,
|
onCurrencySave: (Currencies) -> Unit,
|
||||||
tripName: String,
|
tripName: String,
|
||||||
onExportToCsv: () -> Unit,
|
onExportToCsv: () -> Unit,
|
||||||
|
onCategoriesClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
var showThemeDialog by remember { mutableStateOf(false) }
|
var showThemeDialog by remember { mutableStateOf(false) }
|
||||||
var showCurrencyDialog by remember { mutableStateOf(false) }
|
var showCurrencyDialog by remember { mutableStateOf(false) }
|
||||||
|
var showCategoriesDialog by remember { mutableStateOf(false) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -142,9 +152,15 @@ fun SettingsScreen(
|
|||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
onClick = onExportToCsv,
|
onClick = onExportToCsv,
|
||||||
stringResource(string.export_to_csv),
|
stringResource(string.export_to_csv),
|
||||||
supportingText = "Save expenses from %s to a file".format(tripName),
|
supportingText = stringResource(string.export_csv_subttext).format(tripName),
|
||||||
iconResource = R.drawable.materialsymbols_ic_csv_outlined
|
iconResource = R.drawable.materialsymbols_ic_csv_outlined
|
||||||
)
|
)
|
||||||
|
SettingsListItem(
|
||||||
|
onClick = onCategoriesClick,
|
||||||
|
stringResource(string.categories),
|
||||||
|
supportingText = stringResource(string.manage_categories),
|
||||||
|
iconResource = R.drawable.materialsymbols_ic_label_outlined
|
||||||
|
)
|
||||||
|
|
||||||
if (showThemeDialog) {
|
if (showThemeDialog) {
|
||||||
ThemeSelectionDialog(
|
ThemeSelectionDialog(
|
||||||
@@ -258,7 +274,14 @@ fun ThemeSelectionDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
fun PreviewSettingsScreen() {
|
fun PreviewSettingsScreen() {
|
||||||
TripMoneyTheme {
|
TripMoneyTheme {
|
||||||
SettingsScreen(Currencies.entries.random(), AppTheme.entries.random(), {}, {}, "Włochy", {})
|
SettingsScreen(
|
||||||
|
Currencies.entries.random(),
|
||||||
|
AppTheme.entries.random(),
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
"Włochy",
|
||||||
|
{},
|
||||||
|
{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package cc.n0th1ng.tripmoney.screens.statistics
|
package cc.n0th1ng.tripmoney.screens.statistics
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -13,9 +14,15 @@ 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.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -30,6 +37,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import cc.n0th1ng.tripmoney.R.string
|
||||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
@@ -82,9 +90,10 @@ fun StatisticsScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Summary(summaryAmount: Double, currency: String) {
|
fun Summary(summaryAmount: Double, currency: String) {
|
||||||
Card(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
colors = CardDefaults.elevatedCardColors()
|
||||||
|
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -94,7 +103,10 @@ fun Summary(summaryAmount: Double, currency: String) {
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Text(stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses), style = MaterialTheme.typography.titleSmall)
|
Text(
|
||||||
|
stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses),
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
"%.2f %s".format(summaryAmount, currency),
|
"%.2f %s".format(summaryAmount, currency),
|
||||||
style = MaterialTheme.typography.headlineLarge
|
style = MaterialTheme.typography.headlineLarge
|
||||||
@@ -118,7 +130,11 @@ fun Summary(summaryAmount: Double, currency: String) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
|
fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
ElevatedCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.elevatedCardColors()
|
||||||
|
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(15.dp),
|
modifier = Modifier.padding(15.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(5.dp)
|
verticalArrangement = Arrangement.spacedBy(5.dp)
|
||||||
@@ -191,11 +207,13 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@AllPreviews
|
@AllPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun Preview() {
|
fun Preview() {
|
||||||
TripMoneyTheme {
|
TripMoneyTheme {
|
||||||
|
Scaffold {
|
||||||
StatisticsScreen(
|
StatisticsScreen(
|
||||||
summaryPerCategoryList,
|
summaryPerCategoryList,
|
||||||
summaryAmount = 125.24,
|
summaryAmount = 125.24,
|
||||||
@@ -203,6 +221,7 @@ fun Preview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val categories = listOf(
|
val categories = listOf(
|
||||||
Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()),
|
Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.os.Build
|
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.combinedClickable
|
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
|
||||||
@@ -21,7 +20,6 @@ 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.ExperimentalMaterial3Api
|
||||||
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
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -50,16 +48,21 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
import androidx.paging.compose.itemKey
|
||||||
import cc.n0th1ng.tripmoney.R.string
|
import cc.n0th1ng.tripmoney.R.string
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.navigation.Screens
|
import cc.n0th1ng.tripmoney.navigation.Screens
|
||||||
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
|
||||||
import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog
|
import cc.n0th1ng.tripmoney.screens.listexpense.DeleteConfirmationDialog
|
||||||
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
|
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@@ -70,9 +73,38 @@ fun TripPickerScreen(
|
|||||||
) {
|
) {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
val tripsFlow = tripViewModel.getTrips()
|
||||||
val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems()
|
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
|
|
||||||
|
TripPickerScreen(
|
||||||
|
tripsFlow = tripsFlow,
|
||||||
|
currentTripId = currentTripId,
|
||||||
|
onDelete = { trip -> tripViewModel.delete(trip) },
|
||||||
|
onClick = { trip ->
|
||||||
|
settingsViewModel.setCurrentTrip(trip.id)
|
||||||
|
navController.navigate(Screens.LIST_EXPENSE)
|
||||||
|
},
|
||||||
|
onSave = { trip ->
|
||||||
|
tripViewModel.save(trip)
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@Composable
|
||||||
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
|
fun TripPickerScreen(
|
||||||
|
tripsFlow: Flow<PagingData<Trip>>,
|
||||||
|
currentTripId: Int,
|
||||||
|
onDelete: (Trip) -> Unit,
|
||||||
|
onClick: (Trip) -> Unit,
|
||||||
|
onSave: (Trip) -> Unit
|
||||||
|
) {
|
||||||
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
val trips: LazyPagingItems<Trip> = tripsFlow.collectAsLazyPagingItems()
|
||||||
|
|
||||||
var tripToEdit by remember { mutableStateOf<Trip?>(null) }
|
var tripToEdit by remember { mutableStateOf<Trip?>(null) }
|
||||||
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
@@ -91,11 +123,11 @@ fun TripPickerScreen(
|
|||||||
val trip = trips[i]
|
val trip = trips[i]
|
||||||
if (trip != null) {
|
if (trip != null) {
|
||||||
SwipeToDeleteTripCard(
|
SwipeToDeleteTripCard(
|
||||||
trip, onDelete = {
|
trip = trip,
|
||||||
tripViewModel.delete(trip)
|
onDelete = {
|
||||||
|
onDelete(trip)
|
||||||
}, onClick = {
|
}, onClick = {
|
||||||
settingsViewModel.setCurrentTrip(trip.id)
|
onClick(trip)
|
||||||
navController.navigate(Screens.LIST_EXPENSE)
|
|
||||||
}, isSelected = currentTripId == trip.id,
|
}, isSelected = currentTripId == trip.id,
|
||||||
onLongClick = { trip ->
|
onLongClick = { trip ->
|
||||||
tripToEdit = trip
|
tripToEdit = trip
|
||||||
@@ -113,7 +145,7 @@ fun TripPickerScreen(
|
|||||||
tripToEdit = null
|
tripToEdit = null
|
||||||
},
|
},
|
||||||
onSave = { trip ->
|
onSave = { trip ->
|
||||||
tripViewModel.save(trip)
|
onSave(trip)
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
tripToEdit = null
|
tripToEdit = null
|
||||||
},
|
},
|
||||||
@@ -152,7 +184,6 @@ fun SwipeToDeleteTripCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SwipeToDismissBox(
|
SwipeToDismissBox(
|
||||||
modifier = Modifier.alpha(if (isSelected) 1.0f else 0.7f),
|
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
enableDismissFromStartToEnd = false,
|
enableDismissFromStartToEnd = false,
|
||||||
backgroundContent = {
|
backgroundContent = {
|
||||||
@@ -160,7 +191,7 @@ fun SwipeToDeleteTripCard(
|
|||||||
Modifier
|
Modifier
|
||||||
.clip(CardDefaults.elevatedShape)
|
.clip(CardDefaults.elevatedShape)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.onError)
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
.padding(horizontal = 20.dp),
|
.padding(horizontal = 20.dp),
|
||||||
contentAlignment = Alignment.CenterEnd
|
contentAlignment = Alignment.CenterEnd
|
||||||
) {
|
) {
|
||||||
@@ -186,7 +217,7 @@ fun TripCard(
|
|||||||
containerColor = if (isSelected) {
|
containerColor = if (isSelected) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.secondary
|
MaterialTheme.colorScheme.surfaceContainer
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -195,7 +226,7 @@ fun TripCard(
|
|||||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
onLongClick(trip)
|
onLongClick(trip)
|
||||||
}, onClick = { onClick(trip) }),
|
}, onClick = { onClick(trip) }),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -217,3 +248,38 @@ fun TripCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@AllPreviews
|
||||||
|
@Composable
|
||||||
|
fun PreviewTripPickerScreen() {
|
||||||
|
val tripsToPreview = listOf(
|
||||||
|
Trip(
|
||||||
|
1,
|
||||||
|
name = "Włochy",
|
||||||
|
startDate = LocalDate.parse("2026-03-01"),
|
||||||
|
currency = "PLN"
|
||||||
|
),
|
||||||
|
Trip(
|
||||||
|
2,
|
||||||
|
name = "Szwajcaria",
|
||||||
|
startDate = LocalDate.parse("2025-03-01"),
|
||||||
|
currency = "EUR"
|
||||||
|
),
|
||||||
|
Trip(
|
||||||
|
3,
|
||||||
|
name = "Portugalia",
|
||||||
|
startDate = LocalDate.parse("2025-03-01"),
|
||||||
|
currency = "USD"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
TripMoneyTheme {
|
||||||
|
TripPickerScreen(
|
||||||
|
tripsFlow = MutableStateFlow(PagingData.from(tripsToPreview)),
|
||||||
|
currentTripId = 1,
|
||||||
|
onDelete = {},
|
||||||
|
onClick = {},
|
||||||
|
onSave = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,15 +40,28 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
private val tripRepo: TripRepository
|
private val tripRepo: TripRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
fun getExpensesDtoPaged(tripId: Int): Flow<PagingData<ExpenseDto>> =
|
fun archiveCategory(category: Category) {
|
||||||
expenseRepo.getExpensesDtoPaged(tripId).cachedIn(viewModelScope)
|
viewModelScope.launch {
|
||||||
|
categoryRepo.save(category.copy(archived = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deArchiveCategory(category: Category) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
categoryRepo.save(category.copy(archived = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExpensesDtoPaged(tripId: Int, filter: String = ""): Flow<PagingData<ExpenseDto>> =
|
||||||
|
expenseRepo.getExpensesDtoPaged(tripId, filter).cachedIn(viewModelScope)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun getExpensesWithHeadersPaged(
|
fun getExpensesWithHeadersPaged(
|
||||||
tripId: Int
|
tripId: Int,
|
||||||
|
filter: String = ""
|
||||||
): Flow<PagingData<ExpenseListItemUi>> {
|
): Flow<PagingData<ExpenseListItemUi>> {
|
||||||
val pagingFlow = getExpensesDtoPaged(tripId)
|
val pagingFlow = getExpensesDtoPaged(tripId, filter)
|
||||||
val sumsFlow = getDailySums(tripId)
|
val sumsFlow = getDailySums(tripId, filter)
|
||||||
val tripFlow = tripRepo.getTrip(tripId)
|
val tripFlow = tripRepo.getTrip(tripId)
|
||||||
return combine(pagingFlow, sumsFlow, tripFlow) { pagingData, sums, trip ->
|
return combine(pagingFlow, sumsFlow, tripFlow) { pagingData, sums, trip ->
|
||||||
val currency = trip?.currency ?: ""
|
val currency = trip?.currency ?: ""
|
||||||
@@ -80,8 +93,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
}.cachedIn(viewModelScope)
|
}.cachedIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpensesDto(tripId: Int): Flow<List<ExpenseDto>> =
|
fun getExpensesDto(tripId: Int, filter: String = ""): Flow<List<ExpenseDto>> =
|
||||||
expenseRepo.getExpensesDto(tripId)
|
expenseRepo.getExpensesDto(tripId, filter)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun save(expense: Expense, trip: Trip) {
|
fun save(expense: Expense, trip: Trip) {
|
||||||
@@ -96,14 +109,20 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun delete(expense: Expense) {
|
fun delete(expense: Expense) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
expenseRepo.delete(expense)
|
expenseRepo.delete(expense)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun delete(category: Category) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
categoryRepo.delete(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getCategories(): Flow<List<Category>> = categoryRepo.getCategories()
|
fun getCategories(): Flow<List<Category>> = categoryRepo.getCategories()
|
||||||
|
fun getArchivedCategories(): Flow<List<Category>> = categoryRepo.getArchivedCategories()
|
||||||
|
|
||||||
fun save(category: Category) {
|
fun save(category: Category) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -132,8 +151,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun getDailySums(tripId: Int): Flow<Map<LocalDate, Double>> {
|
fun getDailySums(tripId: Int, filter: String): Flow<Map<LocalDate, Double>> {
|
||||||
return getExpensesDto(tripId)
|
return getExpensesDto(tripId, filter)
|
||||||
.map { expenses ->
|
.map { expenses ->
|
||||||
expenses.groupBy { it.expense.datetime.toLocalDate() }
|
expenses.groupBy { it.expense.datetime.toLocalDate() }
|
||||||
.mapValues { (_, list) ->
|
.mapValues { (_, list) ->
|
||||||
@@ -182,7 +201,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
sealed class ExpenseListItemUi {
|
sealed class ExpenseListItemUi {
|
||||||
data class Item(val expenseDto: ExpenseDto) : ExpenseListItemUi()
|
data class Item(val expenseDto: ExpenseDto) : ExpenseListItemUi()
|
||||||
data class Header(val date: LocalDate, val sum: Double, val currency: String) : ExpenseListItemUi()
|
data class Header(val date: LocalDate, val sum: Double, val currency: String) :
|
||||||
|
ExpenseListItemUi()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
package cc.n0th1ng.tripmoney.viewmodel
|
package cc.n0th1ng.tripmoney.viewmodel
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
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.data.repository.ExpenseRepository
|
||||||
import cc.n0th1ng.tripmoney.data.repository.TripRepository
|
import cc.n0th1ng.tripmoney.data.repository.TripRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TripViewModel @Inject constructor(private val repository: TripRepository) : ViewModel() {
|
class TripViewModel @Inject constructor(
|
||||||
|
private val repository: TripRepository,
|
||||||
|
private val expenseRepository: ExpenseRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _isRecalculating = MutableStateFlow(false)
|
||||||
|
val isRecalculating: StateFlow<Boolean> = _isRecalculating
|
||||||
fun getTrips(): Flow<PagingData<Trip>> = repository.getTrips().cachedIn(viewModelScope)
|
fun getTrips(): Flow<PagingData<Trip>> = repository.getTrips().cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun getTrip(tripId: Int): Flow<Trip?> = repository.getTrip(tripId)
|
fun getTrip(tripId: Int): Flow<Trip?> = repository.getTrip(tripId)
|
||||||
@@ -26,9 +35,15 @@ class TripViewModel @Inject constructor(private val repository: TripRepository)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun save(trip: Trip) {
|
fun save(trip: Trip) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.save(trip)
|
repository.save(trip)
|
||||||
|
_isRecalculating.value = true
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
expenseRepository.recalculateTripExpenses(trip.id)
|
||||||
|
}
|
||||||
|
_isRecalculating.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<string name="save">Zapisz</string>
|
<string name="save">Zapisz</string>
|
||||||
<string name="backspace">Usuń</string>
|
<string name="backspace">Usuń</string>
|
||||||
<string name="pick_category">Wybierz kategorie</string>
|
<string name="pick_category">Wybierz kategorie</string>
|
||||||
<string name="add_new_category">dodaj nową</string>
|
<string name="add_new">dodaj nową</string>
|
||||||
<string name="pick_currency">Wybierz kategorie</string>
|
<string name="pick_currency">Wybierz kategorie</string>
|
||||||
<string name="cancel">Anuluj</string>
|
<string name="cancel">Anuluj</string>
|
||||||
<string name="add_expense">Dodaj wydatek</string>
|
<string name="add_expense">Dodaj wydatek</string>
|
||||||
@@ -27,4 +27,9 @@
|
|||||||
<string name="list_of_expenses">Lista wydatków</string>
|
<string name="list_of_expenses">Lista wydatków</string>
|
||||||
<string name="statistics">Statystyki</string>
|
<string name="statistics">Statystyki</string>
|
||||||
<string name="export_to_csv">Eksport do CSV</string>
|
<string name="export_to_csv">Eksport do CSV</string>
|
||||||
|
<string name="manage_categories">Zarządzaj kategoriami</string>
|
||||||
|
<string name="categories">Kategorie</string>
|
||||||
|
<string name="export_csv_subttext">Zapisz wydatki z %s do pliku</string>
|
||||||
|
<string name="add_new_category">Dodaj kategorie</string>
|
||||||
|
<string name="edit_category">Edytuj kategorie</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="backspace">Backspace</string>
|
<string name="backspace">Backspace</string>
|
||||||
<string name="pick_category">Pick a category</string>
|
<string name="pick_category">Pick a category</string>
|
||||||
<string name="add_new_category">Add new</string>
|
<string name="add_new">Add new</string>
|
||||||
<string name="pick_currency">Pick a currency</string>
|
<string name="pick_currency">Pick a currency</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="add_expense">Add expense</string>
|
<string name="add_expense">Add expense</string>
|
||||||
@@ -27,4 +27,13 @@
|
|||||||
<string name="list_of_expenses">List of expenses</string>
|
<string name="list_of_expenses">List of expenses</string>
|
||||||
<string name="statistics">Statistics</string>
|
<string name="statistics">Statistics</string>
|
||||||
<string name="export_to_csv">Export to CSV</string>
|
<string name="export_to_csv">Export to CSV</string>
|
||||||
|
<string name="export_csv_subttext">Save expenses from %s to a file</string>
|
||||||
|
<string name="manage_categories">Manage categories</string>
|
||||||
|
<string name="categories">Categories</string>
|
||||||
|
<string name="add_new_category">Add category</string>
|
||||||
|
<string name="edit_category">Edit category</string>
|
||||||
|
<string name="archive">Archive</string>
|
||||||
|
<string name="you_want_archive">Do you want to archive?</string>
|
||||||
|
<string name="archive_category_info">No expense will be deleted.</string>
|
||||||
|
<string name="delete_category_info">Your all expenses with category Hotel will be removed.</string>
|
||||||
</resources>
|
</resources>
|
||||||
1
baselineprofile/.gitignore
vendored
Normal file
1
baselineprofile/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
52
baselineprofile/build.gradle.kts
Normal file
52
baselineprofile/build.gradle.kts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.test)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.baselineprofile)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "cc.n0th1ng.baselineprofile"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "11"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 28
|
||||||
|
targetSdk = 36
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProjectPath = ":app"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the configuration block for the Baseline Profile plugin.
|
||||||
|
// You can specify to run the generators on a managed devices or connected devices.
|
||||||
|
baselineProfile {
|
||||||
|
useConnectedDevices = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.junit)
|
||||||
|
implementation(libs.androidx.espresso.core)
|
||||||
|
implementation(libs.androidx.uiautomator)
|
||||||
|
implementation(libs.androidx.benchmark.macro.junit4)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants { v ->
|
||||||
|
val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
|
||||||
|
v.instrumentationRunnerArguments.put(
|
||||||
|
"targetAppId",
|
||||||
|
v.testedApks.map { artifactsLoader.load(it)?.applicationId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
baselineprofile/src/main/AndroidManifest.xml
Normal file
1
baselineprofile/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest />
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cc.n0th1ng.baselineprofile
|
||||||
|
|
||||||
|
import android.R.attr.contentDescription
|
||||||
|
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||||
|
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.LargeTest
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.uiautomator.By
|
||||||
|
import androidx.test.uiautomator.Direction
|
||||||
|
import androidx.test.uiautomator.Until
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@LargeTest
|
||||||
|
class BaselineProfileGenerator {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val rule = BaselineProfileRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun generate() {
|
||||||
|
rule.collect(
|
||||||
|
packageName = "cc.n0th1ng.tripmoney",
|
||||||
|
includeInStartupProfile = true
|
||||||
|
) {
|
||||||
|
pressHome()
|
||||||
|
startActivityAndWait()
|
||||||
|
|
||||||
|
|
||||||
|
// Give Compose time to render
|
||||||
|
Thread.sleep(500)
|
||||||
|
|
||||||
|
val listNav = device.wait(Until.findObject(By.desc("listExpenseScreen")), 10000)
|
||||||
|
listNav?.click() ?: throw RuntimeException("listExpenseScreen not found or not clickable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package cc.n0th1ng.baselineprofile
|
||||||
|
|
||||||
|
import androidx.benchmark.macro.BaselineProfileMode
|
||||||
|
import androidx.benchmark.macro.CompilationMode
|
||||||
|
import androidx.benchmark.macro.StartupMode
|
||||||
|
import androidx.benchmark.macro.StartupTimingMetric
|
||||||
|
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.LargeTest
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test class benchmarks the speed of app startup.
|
||||||
|
* Run this benchmark to verify how effective a Baseline Profile is.
|
||||||
|
* It does this by comparing [CompilationMode.None], which represents the app with no Baseline
|
||||||
|
* Profiles optimizations, and [CompilationMode.Partial], which uses Baseline Profiles.
|
||||||
|
*
|
||||||
|
* Run this benchmark to see startup measurements and captured system traces for verifying
|
||||||
|
* the effectiveness of your Baseline Profiles. You can run it directly from Android
|
||||||
|
* Studio as an instrumentation test, or run all benchmarks for a variant, for example benchmarkRelease,
|
||||||
|
* with this Gradle task:
|
||||||
|
* ```
|
||||||
|
* ./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You should run the benchmarks on a physical device, not an Android emulator, because the
|
||||||
|
* emulator doesn't represent real world performance and shares system resources with its host.
|
||||||
|
*
|
||||||
|
* For more information, see the [Macrobenchmark documentation](https://d.android.com/macrobenchmark#create-macrobenchmark)
|
||||||
|
* and the [instrumentation arguments documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args).
|
||||||
|
**/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@LargeTest
|
||||||
|
class StartupBenchmarks {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val rule = MacrobenchmarkRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startupCompilationNone() =
|
||||||
|
benchmark(CompilationMode.None())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startupCompilationBaselineProfiles() =
|
||||||
|
benchmark(CompilationMode.Partial(BaselineProfileMode.Require))
|
||||||
|
|
||||||
|
private fun benchmark(compilationMode: CompilationMode) {
|
||||||
|
// The application id for the running build variant is read from the instrumentation arguments.
|
||||||
|
rule.measureRepeated(
|
||||||
|
packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
|
||||||
|
?: throw Exception("targetAppId not passed as instrumentation runner arg"),
|
||||||
|
metrics = listOf(StartupTimingMetric()),
|
||||||
|
compilationMode = compilationMode,
|
||||||
|
startupMode = StartupMode.COLD,
|
||||||
|
iterations = 10,
|
||||||
|
setupBlock = {
|
||||||
|
pressHome()
|
||||||
|
},
|
||||||
|
measureBlock = {
|
||||||
|
startActivityAndWait()
|
||||||
|
|
||||||
|
// TODO Add interactions to wait for when your app is fully drawn.
|
||||||
|
// The app is fully drawn when Activity.reportFullyDrawn is called.
|
||||||
|
// For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
|
||||||
|
// from the AndroidX Activity library.
|
||||||
|
|
||||||
|
// Check the UiAutomator documentation for more information on how to
|
||||||
|
// interact with the app.
|
||||||
|
// https://d.android.com/training/testing/other-components/ui-automator
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,6 @@ plugins {
|
|||||||
|
|
||||||
id("com.google.devtools.ksp") version "2.2.21-2.0.5" apply false
|
id("com.google.devtools.ksp") version "2.2.21-2.0.5" apply false
|
||||||
id("com.google.dagger.hilt.android") version "2.57.1" apply false
|
id("com.google.dagger.hilt.android") version "2.57.1" apply false
|
||||||
|
alias(libs.plugins.android.test) apply false
|
||||||
|
alias(libs.plugins.baselineprofile) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ kotlin.code.style=official
|
|||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
@@ -10,6 +10,10 @@ activityCompose = "1.8.0"
|
|||||||
composeBom = "2024.09.00"
|
composeBom = "2024.09.00"
|
||||||
navigationCompose = "2.9.7"
|
navigationCompose = "2.9.7"
|
||||||
foundationLayout = "1.10.5"
|
foundationLayout = "1.10.5"
|
||||||
|
uiautomator = "2.3.0"
|
||||||
|
benchmarkMacroJunit4 = "1.2.4"
|
||||||
|
baselineprofile = "1.2.4"
|
||||||
|
profileinstaller = "1.3.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -28,9 +32,14 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te
|
|||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||||
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
|
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
|
||||||
|
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
||||||
|
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
|
||||||
|
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
android-test = { id = "com.android.test", version.ref = "agp" }
|
||||||
|
baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" }
|
||||||
|
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "tripMoney"
|
rootProject.name = "tripMoney"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":baselineprofile")
|
||||||
|
|||||||
Reference in New Issue
Block a user