Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr_pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
- name: Run Build, Lint and Tests
run: |
set -o pipefail
./gradlew build --no-daemon --stacktrace 2>&1 | tee build_log.txt
./gradlew build -x :webApp:assemble --no-daemon --stacktrace 2>&1 | tee build_log.txt

- name: Check compiler warnings against baseline
run: bash .github/check-compiler-warnings.sh build_log.txt
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ koin = "4.2.2"
kotlin = "2.4.0"
ksafe = "2.1.3"
kotlinx-coroutines = "1.11.0"
kotlinx-browser = "0.5.0"
kotlinx-datetime = "0.8.0"
kotlinx-serialization-json = "1.11.0"
ksp = "2.3.9"
Expand Down Expand Up @@ -63,6 +64,7 @@ koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", ver
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
ksafe = { module = "eu.anifantakis:ksafe", version.ref = "ksafe" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinx-browser" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
Expand All @@ -73,8 +75,10 @@ ktlint-version = { group = "com.pinterest.ktlint", name = "ktlint-ruleset-standa
navigationevent-compose = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent-compose" }
room-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room" }
room-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room" }
sqlite-async = { module = "androidx.sqlite:sqlite-async", version.ref = "sqlite" }
sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
sqlite-web = { module = "androidx.sqlite:sqlite-web", version.ref = "sqlite" }

[plugins]
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" }
Expand Down
18 changes: 18 additions & 0 deletions kotlin-js-store/wasm/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@js-joda/core@3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==

"@sqlite.org/sqlite-wasm@^3.50.1-build1":
version "3.50.1-build1"
resolved "https://registry.yarnpkg.com/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.50.1-build1.tgz#67dd9944b0e37ddb0ef2c8b195baa74ece838e44"
integrity sha512-yH4M/SHN98NibniIwTVk6rwTJjy7n39l7zwWY3u+qsfZBGTi4lC1uEl2NDvIlkzsFtfCBvHBJJFJ1iuU3UzzEQ==

"sqlite-wasm-worker@file:../../webSqliteWorker/worker":
version "0.0.0"
dependencies:
"@sqlite.org/sqlite-wasm" "^3.50.1-build1"
3,380 changes: 3,380 additions & 0 deletions kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ plugins {

include(":app")
include(":shared")
include(":webApp")
include(":webSqliteWorker")
39 changes: 38 additions & 1 deletion shared/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
Expand Down Expand Up @@ -36,6 +37,24 @@ kotlin {

jvm()

js {
browser {
testTask {
// Web unit tests need a browser, which is unavailable on CI.
enabled = false
}
}
}

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser {
testTask {
enabled = false
}
}
}

listOf(
iosArm64(),
iosSimulatorArm64(),
Expand All @@ -48,7 +67,19 @@ kotlin {
}
}

// Explicit call keeps the default template applied despite the manual
// dependsOn edges for nonWebMain below.
applyDefaultHierarchyTemplate()

sourceSets {
// nonWebMain holds code that relies on APIs unavailable on js/wasmJs
// (DataStore, Dispatchers.IO).
val nonWebMain = create("nonWebMain")
nonWebMain.dependsOn(commonMain.get())
androidMain.get().dependsOn(nonWebMain)
jvmMain.get().dependsOn(nonWebMain)
iosMain.get().dependsOn(nonWebMain)

all {
languageSettings.optIn("kotlin.time.ExperimentalTime")
}
Expand All @@ -59,7 +90,6 @@ kotlin {
implementation(libs.sqlite.framework)
}
commonMain.dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.navigation.compose)

Expand All @@ -80,12 +110,17 @@ kotlin {
implementation(libs.aboutlibraries.compose.m3)
implementation(libs.ksafe)
implementation(libs.room.runtime)
implementation(libs.sqlite.async)

implementation(project.dependencies.platform(libs.koin.bom))
api(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
}
getByName("nonWebMain").dependencies {
// Kept only for the one-time migration of legacy preferences into KSafe.
implementation(libs.androidx.datastore.preferences)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
Expand Down Expand Up @@ -137,6 +172,8 @@ configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
dependencies {
add("kspAndroid", libs.room.compiler)
add("kspJvm", libs.room.compiler)
add("kspJs", libs.room.compiler)
add("kspWasmJs", libs.room.compiler)
add("kspIosSimulatorArm64", libs.room.compiler)
add("kspIosArm64", libs.room.compiler)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.dbauer.expensetracker.shared

import kotlinx.coroutines.CoroutineDispatcher

/**
* Dispatcher for IO-bound work. Maps to [kotlinx.coroutines.Dispatchers.IO] on platforms that
* have it and to the default dispatcher on web targets, which have no IO dispatcher.
*/
expect val ioDispatcher: CoroutineDispatcher
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.dbauer.expensetracker.shared.model

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import de.dbauer.expensetracker.shared.ioDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
Expand Down Expand Up @@ -35,7 +34,7 @@ private data class CurrencyWrapper(val data: Map<String, Currency>)
open class CurrencyProvider {
@OptIn(ExperimentalResourceApi::class)
open suspend fun retrieveCurrencies(): List<Currency> =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
val currenciesFile = Res.readBytes("files/currencies.json")
return@withContext Json
.decodeFromString<CurrencyWrapper>(currenciesFile.decodeToString())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package de.dbauer.expensetracker.shared.model

import de.dbauer.expensetracker.shared.data.CurrencyValue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import de.dbauer.expensetracker.shared.ioDispatcher
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -69,7 +68,7 @@ class ExchangeRateProvider : IExchangeRateProvider {

@OptIn(ExperimentalResourceApi::class)
private suspend fun retrieveExchangeRates(): ExchangeRates =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
val currenciesFile = Res.readBytes("files/exchange_rates.json")
return@withContext Json.decodeFromString<ExchangeRates>(currenciesFile.decodeToString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package de.dbauer.expensetracker.shared.model.database

import de.dbauer.expensetracker.shared.data.RecurringExpenseData
import de.dbauer.expensetracker.shared.data.Tag
import de.dbauer.expensetracker.shared.ioDispatcher
import de.dbauer.expensetracker.shared.model.datastore.IUserPreferencesRepository
import de.dbauer.expensetracker.shared.model.getSystemCurrencyCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -37,14 +36,14 @@ class ExpenseRepository(
private val defaultCurrency = userPreferencesRepository.defaultCurrency.get()

override suspend fun getRecurringExpenseById(id: Int): RecurringExpenseData? =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
return@withContext recurringExpenseDao
.getExpenseById(id)
?.toRecurringExpenseData(getDefaultCurrencyCode())
}

override suspend fun insert(recurringExpense: RecurringExpenseData) =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
val expenseId =
recurringExpenseDao.insert(
recurringExpense.toEntryRecurringExpense(getDefaultCurrencyCode()),
Expand All @@ -58,7 +57,7 @@ class ExpenseRepository(
}

override suspend fun update(recurringExpense: RecurringExpenseData) =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
recurringExpenseDao.update(recurringExpense.toEntryRecurringExpense(getDefaultCurrencyCode()))

// Get current tags for the expense
Expand Down Expand Up @@ -95,7 +94,7 @@ class ExpenseRepository(
}

override suspend fun delete(recurringExpense: RecurringExpenseData) =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
recurringExpenseDao.delete(recurringExpense.toEntryRecurringExpense(getDefaultCurrencyCode()))
recurringExpenseDao.deleteAllCrossRefForExpenseId(recurringExpense.id)
recurringExpenseDao.deleteAllRemindersForExpenseId(recurringExpense.id)
Expand All @@ -105,24 +104,24 @@ class ExpenseRepository(
override suspend fun archive(
expenseId: Int,
archivedDateEpoch: Long,
) = withContext(Dispatchers.IO) {
) = withContext(ioDispatcher) {
recurringExpenseDao.setArchivedDate(expenseId, archivedDateEpoch)
}

override suspend fun unarchive(expenseId: Int) =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
recurringExpenseDao.setArchivedDate(expenseId, null)
}

override suspend fun clearEndDateIfOverdue(
expenseId: Int,
nowEpoch: Long,
) = withContext(Dispatchers.IO) {
) = withContext(ioDispatcher) {
recurringExpenseDao.clearEndDateIfOverdue(expenseId, nowEpoch)
}

override suspend fun autoArchiveExpired(nowEpoch: Long): List<AutoArchiveCandidate> =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
val candidates = recurringExpenseDao.getExpensesToAutoArchive(nowEpoch)
candidates.forEach { candidate ->
recurringExpenseDao.setArchivedDate(candidate.id, nowEpoch)
Expand All @@ -131,25 +130,25 @@ class ExpenseRepository(
}

override suspend fun insert(tag: Tag) =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
recurringExpenseDao.insert(tag.toTagEntry())
}

override suspend fun update(tag: Tag) =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
recurringExpenseDao.update(tag.toTagEntry())
}

override suspend fun delete(tag: Tag) =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
recurringExpenseDao.delete(tag.toTagEntry())
recurringExpenseDao.deleteAllCrossRefForTagId(tag.id)
}

override suspend fun markAsPaid(
expenseId: Int,
paymentDateEpoch: Long,
) = withContext(Dispatchers.IO) {
) = withContext(ioDispatcher) {
val existing = recurringExpenseDao.getPaymentRecord(expenseId, paymentDateEpoch)
if (existing == null) {
recurringExpenseDao.insertPaymentRecord(
Expand All @@ -161,15 +160,15 @@ class ExpenseRepository(
override suspend fun markAsUnpaid(
expenseId: Int,
paymentDateEpoch: Long,
) = withContext(Dispatchers.IO) {
) = withContext(ioDispatcher) {
val record = recurringExpenseDao.getPaymentRecord(expenseId, paymentDateEpoch)
if (record != null) {
recurringExpenseDao.deletePaymentRecord(record)
}
}

override suspend fun getPaymentRecordsForExpense(expenseId: Int): List<Long> =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
recurringExpenseDao.getPaymentRecordsForExpense(expenseId).map { it.paymentDate }
}

Expand Down
Loading