This commit is contained in:
Rafal Wisniewski
2026-03-19 15:32:51 +01:00
commit 20370e3906
68 changed files with 3267 additions and 0 deletions

View 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))
}
}

View File

@@ -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>>
}

View File

@@ -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)
}

View 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>
}

View File

@@ -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
)

View File

@@ -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
)

View 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
)

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}