init
This commit is contained in:
105
app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt
Normal file
105
app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt
Normal file
@@ -0,0 +1,105 @@
|
||||
package cc.n0th1ng.tripmoney
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import cc.n0th1ng.tripmoney.data.DatabasePrepopulator
|
||||
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
||||
import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer
|
||||
import cc.n0th1ng.tripmoney.navigation.Screens
|
||||
import cc.n0th1ng.tripmoney.navigation.TopBar
|
||||
import cc.n0th1ng.tripmoney.navigation.TopBarSettings
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.ListExpenseScreen
|
||||
import cc.n0th1ng.tripmoney.screens.settings.SettingsScreen
|
||||
import cc.n0th1ng.tripmoney.screens.statistics.StatisticsScreen
|
||||
import cc.n0th1ng.tripmoney.screens.trippicker.TripPickerScreen
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var databasePrePopulate: DatabasePrepopulator
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
databasePrePopulate.prepopulate()
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
TripMoneyTheme {
|
||||
NavigationDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun NavigationDrawer() {
|
||||
val navController = rememberNavController()
|
||||
val navBackStack by navController.currentBackStackEntryAsState()
|
||||
val current = navBackStack?.destination?.route
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
CustomNavigationDrawer(navController, drawerState) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (current == Screens.SETTINGS) TopBarSettings(
|
||||
navController
|
||||
) else TopBar(onClick = {
|
||||
scope.launch {
|
||||
if (drawerState.isClosed) {
|
||||
drawerState.open()
|
||||
} else {
|
||||
drawerState.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screens.TRIP_PICKER,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable(Screens.LIST_EXPENSE) {
|
||||
ListExpenseScreen()
|
||||
}
|
||||
composable(Screens.TRIP_PICKER) {
|
||||
TripPickerScreen(navController)
|
||||
}
|
||||
composable(Screens.STATISTICS) {
|
||||
StatisticsScreen()
|
||||
}
|
||||
composable(Screens.SETTINGS) {
|
||||
SettingsScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
7
app/src/main/java/cc/n0th1ng/tripmoney/MyApplication.kt
Normal file
7
app/src/main/java/cc/n0th1ng/tripmoney/MyApplication.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package cc.n0th1ng.tripmoney
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class MyApplication: Application()
|
||||
138
app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt
Normal file
138
app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt
Normal file
@@ -0,0 +1,138 @@
|
||||
package cc.n0th1ng.tripmoney.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
||||
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
||||
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Database(entities = [Trip::class, Expense::class, Category::class], version = 1)
|
||||
abstract class TripDatabase : RoomDatabase() {
|
||||
abstract fun tripDao(): TripDao
|
||||
abstract fun expenseDao(): ExpenseDao
|
||||
abstract fun categoryDao(): CategoryDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: TripDatabase? = null
|
||||
|
||||
fun getInstance(context: Context): TripDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
TripDatabase::class.java
|
||||
).allowMainThreadQueries().build().also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTripDatabase(
|
||||
@ApplicationContext context: Context
|
||||
): TripDatabase {
|
||||
return Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
TripDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries() // Only for in-memory DB, not for production!
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideExpenseDao(database: TripDatabase): ExpenseDao {
|
||||
return database.expenseDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTripDao(database: TripDatabase): TripDao {
|
||||
return database.tripDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCategoryDao(database: TripDatabase): CategoryDao {
|
||||
return database.categoryDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabasePrepopulator(
|
||||
tripDao: TripDao,
|
||||
categoryDao: CategoryDao,
|
||||
expenseDao: ExpenseDao
|
||||
): DatabasePrepopulator {
|
||||
return DatabasePrepopulator(tripDao, categoryDao, expenseDao)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DatabasePrepopulator @Inject constructor(
|
||||
private val tripDao: TripDao,
|
||||
private val categoryDao: CategoryDao,
|
||||
private val expenseDao: ExpenseDao
|
||||
) {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend fun prepopulate() {
|
||||
tripDao.insert(Trip(name = "Włochy", startDate = "2025-01-01", currency = "PLN"))
|
||||
tripDao.insert(Trip(name = "Szwajcaria", startDate = "2025-03-01", currency = "EUR"))
|
||||
tripDao.insert(Trip(name = "Portugalia", startDate = "2026-03-01", currency = "USD"))
|
||||
categoryDao.insert(Category(name = "Hotel", icon = Icons.HOTEL, color = "#B3E5FC"))
|
||||
categoryDao.insert(Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = "#C8E6C9"))
|
||||
categoryDao.insert(Category(name = "Transport", icon = Icons.FLIGHT, color = "#FFCDD2"))
|
||||
categoryDao.insert(Category(name = "Rozrywka", icon = Icons.ATTRACTION, color = "#FFF9C4"))
|
||||
categoryDao.insert(Category(name = "Zakupy", icon = Icons.GROCERIES, color = "#E1BEE7"))
|
||||
categoryDao.insert(Category(name = "Zakupy1", icon = Icons.GROCERIES, color = "#D7CCC8"))
|
||||
categoryDao.insert(Category(name = "Zakupy2", icon = Icons.GROCERIES, color = "#BBDEFB"))
|
||||
categoryDao.insert(Category(name = "Zakupy3", icon = Icons.GROCERIES, color = "#D1C4E9"))
|
||||
categoryDao.insert(Category(name = "Zakupy4", icon = Icons.GROCERIES, color = "#DCEDC8"))
|
||||
categoryDao.insert(Category(name = "Zakupy5", icon = Icons.GROCERIES, color = "#F0F4C3"))
|
||||
categoryDao.insert(Category(name = "Zakupy6", icon = Icons.GROCERIES, color = "#FFE0B2"))
|
||||
categoryDao.insert(Category(name = "Zakupy7", icon = Icons.GROCERIES, color = "#D7CCC8"))
|
||||
categoryDao.insert(Category(name = "Zakupy8", icon = Icons.GROCERIES, color = "#CFD8DC"))
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
expenseDao.insert(Expense(amount = 120.50, currency = "PLN", note = "Hotel overnight", datetime = now.minusDays(10).toString(), categoryId = 1, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 45.75, currency = "PLN", note = "Dinner", datetime = now.minusDays(9).toString(), categoryId = 2, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 15.20, currency = "PLN", note = "Bus ticket", datetime = now.minusDays(8).toString(), categoryId = 3, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 89.99, currency = "PLN", note = "Concert tickets", datetime = now.minusDays(7).toString(), categoryId = 4, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 32.50, currency = "PLN", note = "Souvenirs", datetime = now.minusDays(6).toString(), categoryId = 5, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 180.00, currency = "PLN", note = "Hotel 3 nights", datetime = now.minusDays(5).toString(), categoryId = 1, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 67.30, currency = "PLN", note = "Lunch", datetime = now.minusDays(4).toString(), categoryId = 2, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 22.00, currency = "PLN", note = "Train ticket", datetime = now.minusDays(3).toString(), categoryId = 3, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 55.00, currency = "PLN", note = "Museum entry", datetime = now.minusDays(2).toString(), categoryId = 4, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 12.99, currency = "PLN", note = "Snacks", datetime = now.minusDays(1).toString(), categoryId = 2, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 210.00, currency = "PLN", note = "Hotel 5 nights", datetime = now.toString(), categoryId = 1, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 95.50, currency = "EUR", note = "Dinner for two", datetime = now.minusHours(12).toString(), categoryId = 2, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 30.00, currency = "EUR", note = "Taxi", datetime = now.minusHours(6).toString(), categoryId = 3, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 40.00, currency = "USD", note = "Gifts", datetime = now.minusHours(3).toString(), categoryId = 5, tripId = 1))
|
||||
expenseDao.insert(Expense(amount = 75.00, currency = "PLN", note = "Sightseeing tour", datetime = now.minusHours(1).toString(), categoryId = 4, tripId = 1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cc.n0th1ng.tripmoney.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface CategoryDao {
|
||||
@Upsert
|
||||
suspend fun insert(category: Category)
|
||||
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM category
|
||||
"""
|
||||
)
|
||||
fun categories(): Flow<List<Category>>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cc.n0th1ng.tripmoney.data.dao
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
|
||||
@Dao
|
||||
interface ExpenseDao {
|
||||
@Upsert
|
||||
suspend fun insert(expense: Expense)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM expense WHERE trip_id = :tripId
|
||||
ORDER BY DATETIME(expense.datetime) DESC
|
||||
"""
|
||||
)
|
||||
fun expenseDto(tripId: Int): PagingSource<Int, ExpenseDto>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(expense: Expense)
|
||||
}
|
||||
22
app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt
Normal file
22
app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package cc.n0th1ng.tripmoney.data.dao
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
|
||||
@Dao
|
||||
interface TripDao {
|
||||
@Upsert
|
||||
suspend fun insert(trip: Trip)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM trip
|
||||
"""
|
||||
)
|
||||
fun tripsPaged(): PagingSource<Int, Trip>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cc.n0th1ng.tripmoney.data.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
|
||||
@Entity(tableName = "category")
|
||||
data class Category(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo("name") val name: String,
|
||||
@ColumnInfo("icon") val icon: Icons,
|
||||
@ColumnInfo("color") val color: String
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
package cc.n0th1ng.tripmoney.data.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
|
||||
@Entity(tableName = "expense")
|
||||
data class Expense(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo("amount") val amount: Double,
|
||||
@ColumnInfo("currency") val currency: String,
|
||||
@ColumnInfo("note") val note: String,
|
||||
@ColumnInfo("datetime") val datetime: String,
|
||||
@ColumnInfo("category_id") val categoryId: Int,
|
||||
@ColumnInfo("trip_id") val tripId: Int
|
||||
)
|
||||
|
||||
data class ExpenseDto(
|
||||
@Embedded val expense: Expense,
|
||||
@Relation(
|
||||
parentColumn = "category_id",
|
||||
entityColumn = "id"
|
||||
)
|
||||
val category: Category,
|
||||
@Relation(
|
||||
parentColumn = "trip_id",
|
||||
entityColumn = "id"
|
||||
)
|
||||
val trip: Trip
|
||||
)
|
||||
|
||||
13
app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt
Normal file
13
app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package cc.n0th1ng.tripmoney.data.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "trip")
|
||||
data class Trip(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo("name") val name: String,
|
||||
@ColumnInfo("start_date") val startDate: String,
|
||||
@ColumnInfo("currency") val currency: String
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package cc.n0th1ng.tripmoney.data.repository
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryRepository @Inject constructor(private val categoryDao: CategoryDao) {
|
||||
|
||||
@WorkerThread
|
||||
suspend fun save(category: Category) {
|
||||
categoryDao.insert(category)
|
||||
}
|
||||
|
||||
fun getCategories(): Flow<List<Category>> {
|
||||
return categoryDao.categories()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cc.n0th1ng.tripmoney.data.repository
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExpenseRepository @Inject constructor(private val expenseDao: ExpenseDao) {
|
||||
|
||||
@WorkerThread
|
||||
suspend fun save(expense: Expense) {
|
||||
expenseDao.insert(expense)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun delete(expense: Expense) {
|
||||
expenseDao.delete(expense)
|
||||
}
|
||||
|
||||
fun getExpenses(tripId: Int): Flow<PagingData<ExpenseDto>> {
|
||||
return Pager(
|
||||
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
||||
pagingSourceFactory = { expenseDao.expenseDto(tripId) }
|
||||
).flow
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package cc.n0th1ng.tripmoney.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.APP_THEME
|
||||
import cc.n0th1ng.tripmoney.data.repository.PreferenceKeys.CURRENT_TRIP
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
val Context.preferencesDataStore by preferencesDataStore(name = "app_preferences")
|
||||
|
||||
object PreferenceKeys {
|
||||
val APP_THEME = intPreferencesKey("app_theme")
|
||||
val CURRENT_TRIP = intPreferencesKey("current_trip")
|
||||
|
||||
}
|
||||
|
||||
class PreferencesRepository @Inject constructor(@ApplicationContext private val context: Context) {
|
||||
val themeFlow: Flow<AppTheme> =
|
||||
context.preferencesDataStore.data.map { prefs ->
|
||||
val value = prefs[APP_THEME]
|
||||
?: AppTheme.SYSTEM.value
|
||||
AppTheme.fromValue(value)
|
||||
}
|
||||
|
||||
val currentTripFlow: Flow<Int> =
|
||||
context.preferencesDataStore.data.map { prefs ->
|
||||
prefs[CURRENT_TRIP] ?: -1
|
||||
}
|
||||
|
||||
suspend fun saveCurrentTrip(tripId: Int) {
|
||||
context.preferencesDataStore.edit { prefs ->
|
||||
prefs[CURRENT_TRIP] = tripId
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveTheme(theme: AppTheme) {
|
||||
context.preferencesDataStore.edit { prefs ->
|
||||
prefs[APP_THEME] = theme.value
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class AppTheme(val value: Int) {
|
||||
LIGHT(0), DARK(1), SYSTEM(2);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int) =
|
||||
entries.firstOrNull { it.value == value } ?: SYSTEM
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cc.n0th1ng.tripmoney.data.repository
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class TripRepository @Inject constructor(private val tripDao: TripDao) {
|
||||
|
||||
@WorkerThread
|
||||
suspend fun save(trip: Trip) {
|
||||
tripDao.insert(trip)
|
||||
}
|
||||
|
||||
fun getTrips(): Flow<PagingData<Trip>> {
|
||||
return Pager(
|
||||
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
||||
pagingSourceFactory = { tripDao.tripsPaged() }
|
||||
).flow
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package cc.n0th1ng.tripmoney.navigation
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
|
||||
@Composable
|
||||
fun BottomNavigation(navController: NavController) {
|
||||
val navBackStack by navController.currentBackStackEntryAsState()
|
||||
val current = navBackStack?.destination?.route
|
||||
|
||||
NavigationBar {
|
||||
NavigationBarItem(
|
||||
selected = current?.contains(Screens.TRIP_PICKER) == true,
|
||||
onClick = { navController.navigate(Screens.TRIP_PICKER) },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
R.drawable.materialsymbols_ic_luggage_outlined
|
||||
), "trip picker"
|
||||
)
|
||||
}
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = current?.contains(Screens.LIST_EXPENSE) == true,
|
||||
onClick = { navController.navigate(Screens.LIST_EXPENSE) },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
R.drawable.materialsymbols_ic_list_outlined,
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
NavigationBarItem(
|
||||
selected = current?.contains(Screens.STATISTICS) == true,
|
||||
onClick = { navController.navigate(Screens.STATISTICS) },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
R.drawable.materialsymbols_ic_pie_chart_outlined,
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package cc.n0th1ng.tripmoney.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun CustomNavigationDrawer(
|
||||
navController: NavController, drawerState: DrawerState, content: @Composable () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState, drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Text("Trip Money", modifier = Modifier.padding(16.dp))
|
||||
HorizontalDivider()
|
||||
NavigationDrawerItem(
|
||||
label = { Text(text = "Pick trip") },
|
||||
selected = false,
|
||||
onClick = {
|
||||
navController.navigate(Screens.TRIP_PICKER)
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
R.drawable.materialsymbols_ic_luggage_outlined,
|
||||
), null
|
||||
)
|
||||
})
|
||||
NavigationDrawerItem(
|
||||
label = { Text(text = "List of expenses") },
|
||||
selected = false,
|
||||
onClick = {
|
||||
navController.navigate(Screens.LIST_EXPENSE)
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
R.drawable.materialsymbols_ic_list_outlined,
|
||||
), null
|
||||
)
|
||||
})
|
||||
NavigationDrawerItem(
|
||||
label = { Text(text = "Statistics") },
|
||||
selected = false,
|
||||
onClick = {
|
||||
navController.navigate(Screens.STATISTICS)
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
R.drawable.materialsymbols_ic_pie_chart_outlined,
|
||||
), null
|
||||
)
|
||||
})
|
||||
NavigationDrawerItem(
|
||||
label = { Text(text = "Settings") },
|
||||
selected = false,
|
||||
onClick = {
|
||||
navController.navigate(Screens.SETTINGS)
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Default.Settings, contentDescription = "settings") }
|
||||
)
|
||||
}
|
||||
}) { content() }
|
||||
}
|
||||
|
||||
object Screens {
|
||||
const val LIST_EXPENSE = "list_expense"
|
||||
const val TRIP_PICKER = "trip_picker"
|
||||
const val STATISTICS = "statistics"
|
||||
const val SETTINGS = "settings"
|
||||
}
|
||||
41
app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt
Normal file
41
app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package cc.n0th1ng.tripmoney.navigation
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopBar(onClick: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onClick() }) {
|
||||
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopBarSettings(navController: NavHostController) {
|
||||
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package cc.n0th1ng.tripmoney.screens
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
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.aspectRatio
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.Colors
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
|
||||
@Composable
|
||||
fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var icon by remember { mutableStateOf(Icons.entries[0]) }
|
||||
var color by remember { mutableStateOf(Colors.entries[0].hexString) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss, title = { Text("Add new category") }, text = {
|
||||
AlertDialogFill(
|
||||
onTextChange = { newText ->
|
||||
name = newText
|
||||
},
|
||||
onIconChange = { newIcon -> icon = newIcon },
|
||||
onColorChange = {newColor -> color = newColor}
|
||||
)
|
||||
}, confirmButton = {
|
||||
Button(
|
||||
enabled = !name.isEmpty(),
|
||||
onClick = {
|
||||
onSave(
|
||||
Category(
|
||||
name = name,
|
||||
icon = icon,
|
||||
color = color
|
||||
)
|
||||
)
|
||||
}) { Text("Save") }
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.error),
|
||||
onClick = onDismiss
|
||||
) { Text("close") }
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Unit, onColorChange: (String) -> Unit) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
var iconId by remember { mutableIntStateOf(Icons.entries[0].resource) }
|
||||
var colorHex by remember { mutableStateOf(Colors.entries[0].hexString) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(30.dp),
|
||||
painter = painterResource(iconId), contentDescription = null,
|
||||
tint = Color(colorHex.toColorInt())
|
||||
)
|
||||
OutlinedTextField(label = { Text("Name") }, value = text, onValueChange = { newText ->
|
||||
text = newText
|
||||
onTextChange(text)
|
||||
})
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.horizontalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
) {
|
||||
Icons.entries.forEach { icon ->
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clickable(onClick = {
|
||||
iconId = icon.resource
|
||||
onIconChange(icon)
|
||||
}),
|
||||
painter = painterResource(icon.resource),
|
||||
contentDescription = null,
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.horizontalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
) {
|
||||
Colors.entries.forEach { color ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = {
|
||||
colorHex = color.hexString
|
||||
onColorChange(colorHex)
|
||||
})
|
||||
.size(30.dp)
|
||||
.aspectRatio(1f)
|
||||
.background(Color(color.hexString.toColorInt()))
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package cc.n0th1ng.tripmoney.screens.addexpense
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.CategorySelectionDialog
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.DateTimePicker
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import java.time.LocalDateTime
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun AddExpenseBottomSheet(
|
||||
onSave: (Expense) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
categories: List<Category>,
|
||||
expenseAndCategoryViewModel: ExpenseAndCategoryViewModel,
|
||||
expenseDtoToEdit: ExpenseDto?
|
||||
) {
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
var amount by remember {
|
||||
mutableStateOf(
|
||||
expenseDtoToEdit?.expense?.amount?.toString() ?: "0.00"
|
||||
)
|
||||
}
|
||||
var showCurrencyDialog by remember { mutableStateOf(false) }
|
||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||
var currency by remember { mutableStateOf(expenseDtoToEdit?.expense?.currency ?: "PLN") }
|
||||
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
|
||||
var datetime by remember {
|
||||
mutableStateOf(
|
||||
LocalDateTime.parse(expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now().toString())
|
||||
)
|
||||
}
|
||||
var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") }
|
||||
var enableSave by remember { mutableStateOf(expenseDtoToEdit != null) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp)
|
||||
) {
|
||||
Text(
|
||||
text = amount.ifEmpty { "0.00" },
|
||||
fontSize = 25.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
CurrencyButton(onClick = { showCurrencyDialog = true }, text = currency)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
DateTimePicker(
|
||||
dateTime = datetime,
|
||||
onChange = { datetime = it }
|
||||
)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
CategoryButton(onClick = { showCategoryDialog = true }, category = category)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
Row(
|
||||
modifier = Modifier.height(50.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
NoteInput(note = note) { newNote -> note = newNote }
|
||||
SaveButton(
|
||||
enabled = enableSave,
|
||||
onClick = {
|
||||
val expenseToSave = Expense(
|
||||
amount = amount.toDouble(),
|
||||
currency = currency,
|
||||
note = note,
|
||||
datetime = datetime.toString(),
|
||||
categoryId = category.id,
|
||||
tripId = currentTripId
|
||||
)
|
||||
onSave(
|
||||
if (expenseDtoToEdit == null) expenseToSave
|
||||
else expenseToSave.copy(id = expenseDtoToEdit.expense.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
NumberKeyboard(
|
||||
onNumberClick = { number ->
|
||||
val newText = (if (amount == "0.00") "" else amount) + number
|
||||
if (newText.isDoubleTwoDigitsAboveZero()) {
|
||||
amount = newText
|
||||
enableSave = true
|
||||
} else if (amount == "0.00") {
|
||||
enableSave = false
|
||||
}
|
||||
|
||||
},
|
||||
onBackspaceClick = {
|
||||
if (amount == "0.00") return@NumberKeyboard
|
||||
amount = amount.safeSubstring(0, amount.length - 1)
|
||||
enableSave = amount.isDoubleTwoDigitsAboveZero()
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (showCurrencyDialog) {
|
||||
CurrencySelectionDialog(
|
||||
onDismiss = { showCurrencyDialog = false },
|
||||
onCurrencySelected = { selectedCurrency ->
|
||||
showCurrencyDialog = false
|
||||
currency = selectedCurrency
|
||||
},
|
||||
selected = currency,
|
||||
listOfCurrencies = listOf("PLN", "EUR", "USD")
|
||||
)
|
||||
}
|
||||
|
||||
if (showCategoryDialog) {
|
||||
CategorySelectionDialog(
|
||||
onDismiss = { showCategoryDialog = false },
|
||||
onCategorySelected = { selectedCategory ->
|
||||
showCategoryDialog = false
|
||||
category = selectedCategory
|
||||
},
|
||||
selected = category,
|
||||
categories = categories,
|
||||
settingsAndCategoryViewModel = expenseAndCategoryViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.safeSubstring(start: Int, end: Int): String {
|
||||
return try {
|
||||
this.substring(start, end)
|
||||
} catch (e: Exception) {
|
||||
"0.00"
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isDoubleTwoDigitsAboveZero(): Boolean {
|
||||
return this.toDoubleOrNull() != null && this.matches(Regex("^\\d*(\\.\\d{0,2})?$")) && this.toDouble() > 0
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteInput(note: String, onTextChange: (String) -> Unit) {
|
||||
var text by remember { mutableStateOf(note) }
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(stringResource(R.string.note)) }, value = note, onValueChange = { newText ->
|
||||
text = newText
|
||||
onTextChange(text)
|
||||
}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrencyButton(onClick: () -> Unit, text: String) {
|
||||
OutlinedButton(onClick = onClick) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryButton(onClick: () -> Unit, category: Category) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
painter = painterResource(category.icon.resource),
|
||||
contentDescription = stringResource(R.string.category),
|
||||
tint = Color(category.color.toColorInt())
|
||||
)
|
||||
Text(category.name, color = Color(category.color.toColorInt()))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaveButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = Modifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = stringResource(R.string.save)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun Preview() {
|
||||
TripMoneyTheme(darkTheme = true) {
|
||||
NumberKeyboard(onNumberClick = {}, onBackspaceClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun NumberKeyboard(
|
||||
modifier: Modifier = Modifier,
|
||||
onNumberClick: (String) -> Unit,
|
||||
onBackspaceClick: () -> Unit
|
||||
) {
|
||||
val buttonModifier = Modifier
|
||||
.padding(4.dp)
|
||||
.aspectRatio(2f)
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("1") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("1", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("2") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("2", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("3") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("3", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("4") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("4", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("5") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("5", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("6") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("6", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("7") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("7", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("8") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("8", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("9") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("9", fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick(".") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text(".", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNumberClick("0") },
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Text("0", fontSize = 20.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onBackspaceClick,
|
||||
modifier = buttonModifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.backspace)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
|
||||
@Composable
|
||||
fun CategorySelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onCategorySelected: (Category) -> Unit,
|
||||
selected: Category,
|
||||
categories: List<Category>,
|
||||
settingsAndCategoryViewModel: ExpenseAndCategoryViewModel
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
var showAddCategoryDialog by remember { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss, title = { Text(stringResource(string.pick_category)) }, text = {
|
||||
Column {
|
||||
LazyColumn(
|
||||
modifier = Modifier.heightIn(max = 300.dp),
|
||||
state = listState,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
items(
|
||||
count = categories.size,
|
||||
key = { index -> categories[index].id }) { index ->
|
||||
val category = categories[index]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onCategorySelected(category)
|
||||
}
|
||||
.padding(vertical = 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = selected == category, onClick = {
|
||||
onCategorySelected(category)
|
||||
})
|
||||
Icon(
|
||||
painter = painterResource(category.icon.resource),
|
||||
contentDescription = stringResource(string.category),
|
||||
tint = Color(category.color.toColorInt())
|
||||
)
|
||||
Text(
|
||||
text = category.name, modifier = Modifier.padding(start = 8.dp),
|
||||
color = Color(category.color.toColorInt())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showAddCategoryDialog = true
|
||||
}
|
||||
.padding(top = 15.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.materialsymbols_ic_add_outlined),
|
||||
contentDescription = stringResource(string.category)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(string.add_new_category), modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}, confirmButton = {})
|
||||
if (showAddCategoryDialog) {
|
||||
AddCategoryDialog(onDismiss = {
|
||||
showAddCategoryDialog = false
|
||||
}, onSave = { category ->
|
||||
settingsAndCategoryViewModel.save(category)
|
||||
showAddCategoryDialog = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cc.n0th1ng.tripmoney.R
|
||||
|
||||
@Composable
|
||||
fun CurrencySelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onCurrencySelected: (String) -> Unit,
|
||||
selected: String,
|
||||
listOfCurrencies: List<String>
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.pick_currency)) },
|
||||
text = {
|
||||
Column {
|
||||
listOfCurrencies.forEach { currency ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onCurrencySelected(currency)
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = selected == currency, onClick = {
|
||||
onCurrencySelected(currency)
|
||||
})
|
||||
Text(
|
||||
text = currency, modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {})
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TimePicker
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.material3.rememberTimePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun DateTimePicker(
|
||||
dateTime: LocalDateTime = LocalDateTime.now(),
|
||||
onChange: (LocalDateTime) -> Unit
|
||||
) {
|
||||
val datePickerState =
|
||||
rememberDatePickerState(initialSelectedDateMillis = dateTime.toEpochMilli())
|
||||
val timePickerState = rememberTimePickerState(
|
||||
initialHour = dateTime.hour,
|
||||
initialMinute = dateTime.minute
|
||||
)
|
||||
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
var showTimePicker by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
val formatter = DateTimeFormatter.ofPattern("dd.MM HH:mm")
|
||||
OutlinedButton(onClick = { showDatePicker = true }) {
|
||||
Text(text = dateTime.format(formatter), fontSize = 17.sp)
|
||||
}
|
||||
|
||||
if (showDatePicker) {
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showDatePicker = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showDatePicker = false
|
||||
val selectedMillis = datePickerState.selectedDateMillis
|
||||
if (selectedMillis != null) {
|
||||
val selectedDate = Instant.ofEpochMilli(selectedMillis)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
// open time picker next
|
||||
showTimePicker = true
|
||||
onChange(
|
||||
LocalDateTime.of(
|
||||
selectedDate,
|
||||
dateTime.toLocalTime()
|
||||
)
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Text("OK")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
showDatePicker = false
|
||||
}) { Text(stringResource(string.cancel)) }
|
||||
}
|
||||
) {
|
||||
DatePicker(state = datePickerState)
|
||||
}
|
||||
}
|
||||
|
||||
if (showTimePicker) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showTimePicker = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showTimePicker = false
|
||||
val newTime = LocalTime.of(timePickerState.hour, timePickerState.minute)
|
||||
onChange(LocalDateTime.of(dateTime.toLocalDate(), newTime))
|
||||
}) {
|
||||
Text("OK")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showTimePicker = false }) { Text(stringResource(string.cancel)) }
|
||||
},
|
||||
text = { TimePicker(state = timePickerState) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun LocalDateTime.toEpochMilli(): Long =
|
||||
this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||
@@ -0,0 +1,328 @@
|
||||
package cc.n0th1ng.tripmoney.screens.listexpense
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.rememberLazyListState
|
||||
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.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.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.getValue
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ListExpenseScreen() {
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
|
||||
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
||||
val categories by expenseAndCategoryViewModel.getCategories()
|
||||
.collectAsState(initial = emptyList())
|
||||
val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems()
|
||||
val listState = rememberLazyListState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
var expenseDtoToEdit: ExpenseDto? = null
|
||||
|
||||
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { showBottomSheet = true },
|
||||
icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) },
|
||||
text = { Text(text = stringResource(string.add_expense)) },
|
||||
)
|
||||
})
|
||||
{
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = listState
|
||||
) {
|
||||
items(
|
||||
count = expenses.itemCount,
|
||||
key = { index -> expenses[index]?.expense?.id ?: index }
|
||||
) { index ->
|
||||
val expenseDto = expenses[index]
|
||||
if (expenseDto != null) {
|
||||
val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1)
|
||||
|
||||
val showDayDivider =
|
||||
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
|
||||
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
|
||||
.toLocalDate()
|
||||
Spacer(Modifier.height(5.dp))
|
||||
if (showDayDivider) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Absolute.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
LocalDateTime.parse(expenseDto.expense.datetime).format(
|
||||
DateTimeFormatter.ofPattern("dd EEEE")
|
||||
).toString(),
|
||||
modifier = Modifier.background(Color.White.copy(alpha = 0f))
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
SwipeToDeleteExpenseCard(
|
||||
expenseDto = expenseDto,
|
||||
onDelete = { expense -> expenseAndCategoryViewModel.delete(expense) },
|
||||
onClick = { expenseDto ->
|
||||
expenseDtoToEdit = expenseDto
|
||||
showBottomSheet = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (showBottomSheet) {
|
||||
AddExpenseBottomSheet(
|
||||
onSave = { expense ->
|
||||
expenseAndCategoryViewModel.save(expense)
|
||||
showBottomSheet = false
|
||||
expenseDtoToEdit = null
|
||||
},
|
||||
onDismiss = {
|
||||
expenseDtoToEdit = null
|
||||
showBottomSheet = false
|
||||
},
|
||||
settingsViewModel = settingsViewModel,
|
||||
categories = categories,
|
||||
expenseAndCategoryViewModel = expenseAndCategoryViewModel,
|
||||
expenseDtoToEdit = expenseDtoToEdit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun SwipeToDeleteExpenseCard(
|
||||
expenseDto: ExpenseDto,
|
||||
onDelete: (Expense) -> Unit,
|
||||
onClick: (ExpenseDto) -> Unit
|
||||
) {
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (!dismissed) {
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = { dismissValue ->
|
||||
if (dismissValue == SwipeToDismissBoxValue.EndToStart
|
||||
) {
|
||||
showDialog = true
|
||||
false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
)
|
||||
if (showDialog) {
|
||||
DeleteConfirmationDialog(
|
||||
onConfirm = {
|
||||
showDialog = false
|
||||
dismissed = true
|
||||
onDelete(expenseDto.expense)
|
||||
},
|
||||
onCancel = { showDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
SwipeToDismissBox(
|
||||
modifier = Modifier,
|
||||
state = dismissState,
|
||||
enableDismissFromStartToEnd = false,
|
||||
backgroundContent = {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CardDefaults.elevatedShape)
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.onError)
|
||||
.padding(horizontal = 20.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete))
|
||||
}
|
||||
}
|
||||
) {
|
||||
ExpenseCard(expenseDto, onClick = onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DeleteConfirmationDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { onCancel() }
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(string.delete_confirmation),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(string.cancel),
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp)
|
||||
.clickable { onCancel() }
|
||||
)
|
||||
Text(
|
||||
text = stringResource(string.delete),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable { onConfirm() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(70.dp)
|
||||
.clickable { onClick(expenseDto) },
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 7.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(15.dp),
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(expenseDto.category.icon.resource),
|
||||
contentDescription = "Category",
|
||||
tint = Color(expenseDto.category.color.toColorInt())
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
) {
|
||||
Text(
|
||||
text = expenseDto.category.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
lineHeight = 5.sp
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(0.dp),
|
||||
text = expenseDto.expense.note,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 5.sp
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = LocalDateTime.parse(expenseDto.expense.datetime).format(
|
||||
DateTimeFormatter.ofPattern("dd MMM HH:mm")
|
||||
),
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "- %.2f ${expenseDto.expense.currency}".format(expenseDto.expense.amount),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
|
||||
Text(
|
||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.amount),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package cc.n0th1ng.tripmoney.screens.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
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.rememberCoroutineScope
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
fun SettingsScreen() {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val currentTheme by settingsViewModel.theme.collectAsState()
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(15.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Card {
|
||||
SettingsListItem(onClick = { showDialog = true }, stringResource(string.theme)) {
|
||||
Text(
|
||||
if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource(
|
||||
string.light_theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
ThemeSelectionDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
onThemeSelected = { theme ->
|
||||
settingsViewModel.setTheme(theme)
|
||||
showDialog = false
|
||||
},
|
||||
selected = currentTheme
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsCard(@StringRes title: Int = -1, content: @Composable () -> Unit) {
|
||||
Card {
|
||||
if (title != -1) {
|
||||
Text(
|
||||
text = stringResource(title),
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier
|
||||
.padding(start = 15.dp, top = 15.dp, end = 15.dp)
|
||||
.alpha(0.6f)
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsListItem(
|
||||
onClick: () -> Unit,
|
||||
headlineText: String,
|
||||
trailingContent: @Composable () -> Unit = {},
|
||||
supportingContent: @Composable () -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
headlineContent = { Text(headlineText) },
|
||||
supportingContent = supportingContent,
|
||||
trailingContent = trailingContent,
|
||||
modifier = Modifier
|
||||
.clickable(true, onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeSelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onThemeSelected: (AppTheme) -> Unit,
|
||||
selected: AppTheme
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(string.pick_theme)) },
|
||||
text = {
|
||||
Column {
|
||||
AppTheme.entries.forEach { theme ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onThemeSelected(theme)
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected == theme,
|
||||
onClick = {
|
||||
onThemeSelected(theme)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = when (theme) {
|
||||
AppTheme.LIGHT -> stringResource(string.light_theme)
|
||||
AppTheme.DARK -> stringResource(string.dark_theme)
|
||||
AppTheme.SYSTEM -> stringResource(string.system_settings)
|
||||
},
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cc.n0th1ng.tripmoney.screens.statistics
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun StatisticsScreen() {
|
||||
Text("TODO")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package cc.n0th1ng.tripmoney.screens.trippicker
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.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.runtime.Composable
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import cc.n0th1ng.tripmoney.navigation.Screens
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
|
||||
@Composable
|
||||
fun TripPickerScreen(
|
||||
navController: NavController
|
||||
) {
|
||||
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val tripViewModel: TripViewModel = hiltViewModel()
|
||||
val trips: LazyPagingItems<Trip> = tripViewModel.getTrips().collectAsLazyPagingItems()
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 15.dp)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
||||
items(trips.itemCount, trips.itemKey { it.id }) { i ->
|
||||
Spacer(Modifier.height(10.dp))
|
||||
val trip = trips[i]
|
||||
if (trip != null) {
|
||||
TripCard(trip, currentTripId == trip.id, onClick = {
|
||||
settingsViewModel.setCurrentTrip(trip.id)
|
||||
navController.navigate(Screens.LIST_EXPENSE)
|
||||
})
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TripCard(trip: Trip, isSelected: Boolean, onClick: () -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.height(100.dp)
|
||||
.clickable(true, onClick = onClick)
|
||||
.alpha(if (isSelected) 1.0f else 0.7f),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 7.dp else 0.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name)
|
||||
Text(trip.startDate)
|
||||
}
|
||||
Text(
|
||||
trip.currency.uppercase(),
|
||||
modifier = Modifier.padding(20.dp),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/cc/n0th1ng/tripmoney/theme/Color.kt
Normal file
11
app/src/main/java/cc/n0th1ng/tripmoney/theme/Color.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package cc.n0th1ng.tripmoney.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
57
app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt
Normal file
57
app/src/main/java/cc/n0th1ng/tripmoney/theme/Theme.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
package cc.n0th1ng.tripmoney.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TripMoneyTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/cc/n0th1ng/tripmoney/theme/Type.kt
Normal file
34
app/src/main/java/cc/n0th1ng/tripmoney/theme/Type.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package cc.n0th1ng.tripmoney.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
25
app/src/main/java/cc/n0th1ng/tripmoney/utils/Colors.kt
Normal file
25
app/src/main/java/cc/n0th1ng/tripmoney/utils/Colors.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package cc.n0th1ng.tripmoney.utils
|
||||
|
||||
enum class Colors(val hexString: String) {
|
||||
RED("#D53E0F"),
|
||||
PINK("#FF69B4"),
|
||||
ORANGE("#FF8C00"),
|
||||
YELLOW("#FFD700"),
|
||||
LIME("#32CD32"),
|
||||
GREEN("#228B22"),
|
||||
MINT("#98FB98"),
|
||||
TEAL("#008080"),
|
||||
CYAN("#00CED1"),
|
||||
SKY_BLUE("#1E90FF"),
|
||||
BLUE("#0000FF"),
|
||||
LAVENDER("#8A2BE2"),
|
||||
LILAC("#C8A2C8"),
|
||||
PURPLE("#800080"),
|
||||
MAUVE("#D8BFD8"),
|
||||
MAGENTA("#FF00FF"),
|
||||
VIOLET("#9400D3"),
|
||||
INDIGO("#4B0082"),
|
||||
PERIWINKLE("#8A2BE2"),
|
||||
GRAY("#696969");
|
||||
}
|
||||
|
||||
22
app/src/main/java/cc/n0th1ng/tripmoney/utils/Icons.kt
Normal file
22
app/src/main/java/cc/n0th1ng/tripmoney/utils/Icons.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package cc.n0th1ng.tripmoney.utils
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
|
||||
|
||||
enum class Icons(@DrawableRes val resource: Int) {
|
||||
HOTEL(R.drawable.materialsymbols_ic_hotel_outlined),
|
||||
RESTAURANT(R.drawable.materialsymbols_ic_restaurant_outlined),
|
||||
FLIGHT(R.drawable.materialsymbols_ic_flight_outlined),
|
||||
ATTRACTION(R.drawable.materialsymbols_ic_museum_outlined),
|
||||
GROCERIES(R.drawable.materialsymbols_ic_grocery_outlined),
|
||||
GROCERIES1(R.drawable.materialsymbols_ic_airline_seat_recline_normal_outlined),
|
||||
GROCERIES2(R.drawable.materialsymbols_ic_grocery_outlined),
|
||||
GROCERIES3(R.drawable.materialsymbols_ic_grocery_outlined),
|
||||
GROCERIES4(R.drawable.materialsymbols_ic_grocery_outlined),
|
||||
GROCERIES5(R.drawable.materialsymbols_ic_grocery_outlined),
|
||||
GROCERIES6(R.drawable.materialsymbols_ic_grocery_outlined),
|
||||
GROCERIES7(R.drawable.materialsymbols_ic_grocery_outlined)
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cc.n0th1ng.tripmoney.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import cc.n0th1ng.tripmoney.data.repository.CategoryRepository
|
||||
import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
private val expenseRepo: ExpenseRepository,
|
||||
private val categoryRepo: CategoryRepository
|
||||
) : ViewModel() {
|
||||
|
||||
fun getExpenses(tripId: Int): Flow<PagingData<ExpenseDto>> =
|
||||
expenseRepo.getExpenses(tripId).cachedIn(viewModelScope)
|
||||
|
||||
fun save(expense: Expense) {
|
||||
viewModelScope.launch {
|
||||
expenseRepo.save(expense)
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(expense: Expense) {
|
||||
viewModelScope.launch {
|
||||
expenseRepo.delete(expense)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategories(): Flow<List<Category>> = categoryRepo.getCategories()
|
||||
|
||||
fun save(category: Category) {
|
||||
viewModelScope.launch {
|
||||
categoryRepo.save(category)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cc.n0th1ng.tripmoney.viewmodel
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
||||
import cc.n0th1ng.tripmoney.data.repository.PreferencesRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val repo: PreferencesRepository,
|
||||
@ApplicationContext private val context: Context
|
||||
) : ViewModel() {
|
||||
private val uiModeManager: UiModeManager =
|
||||
context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
val theme = repo.themeFlow
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
AppTheme.SYSTEM
|
||||
)
|
||||
|
||||
val currentTrip = repo.currentTripFlow.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(5000),
|
||||
-1
|
||||
)
|
||||
|
||||
fun setCurrentTrip(tripId: Int) {
|
||||
viewModelScope.launch {
|
||||
repo.saveCurrentTrip(tripId)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
fun setTheme(theme: AppTheme) {
|
||||
applyTheme(theme)
|
||||
|
||||
viewModelScope.launch {
|
||||
repo.saveTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private fun applyTheme(theme: AppTheme) {
|
||||
when (theme) {
|
||||
AppTheme.LIGHT ->
|
||||
uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_NO)
|
||||
|
||||
AppTheme.DARK ->
|
||||
uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
|
||||
|
||||
AppTheme.SYSTEM ->
|
||||
uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package cc.n0th1ng.tripmoney.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.data.repository.TripRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TripViewModel @Inject constructor(private val repository: TripRepository) : ViewModel() {
|
||||
|
||||
fun getTrips(): Flow<PagingData<Trip>> = repository.getTrips().cachedIn(viewModelScope)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user