Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{kt,kts}]
indent_size = 4
indent_style = space
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable

[shared/src/*Main/kotlin/org/mpc/core/*.kt]
# Factory files are organized by function and use KMP target suffixes, so their
# names intentionally do not mirror one declaration in PascalCase.
ktlint_standard_filename = disabled

[shared/src/commonMain/kotlin/org/mpc/presentation/icon/*.kt]
# These are generated-style Material icon definitions whose private cache names
# intentionally follow the upstream icon source rather than property conventions.
ktlint_standard_backing-property-naming = disabled
65 changes: 65 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Lint

on:
pull_request:
push:
branches:
- dev
- main

permissions:
contents: read

concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true

jobs:
kotlin:
name: Detekt and ktlint
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

# required for gradle 9.5.0 and android gradle plugin 9.3.0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-provider: basic

- name: Run Detekt and ktlint
run: >-
./gradlew
ktlintCheck
:shared:detektMainAndroid
:shared:detektHostTestAndroid
:androidApp:detektDebug
--no-daemon

swift:
name: SwiftLint
runs-on: macos-latest
timeout-minutes: 10

steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Install SwiftLint
run: brew install swiftlint

- name: Run SwiftLint
run: swiftlint lint --config .swiftlint.yml --no-cache iosApp/iosApp
10 changes: 10 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
alias(libs.plugins.testBalloon)
}

Expand Down Expand Up @@ -72,3 +73,12 @@ detekt {
config.setFrom(rootProject.file("config/detekt/detekt.yml"))
buildUponDefaultConfig = true
}

ktlint {
version.set(libs.versions.ktlint)
filter {
exclude {
"/build/generated/" in it.file.invariantSeparatorsPath
}
}
}
6 changes: 3 additions & 3 deletions androidApp/src/main/kotlin/org/mpc/AndroidShell.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ fun AndroidAppShell(appGraph: AppGraph) {
) { paddingValues ->
Box(
modifier =
Modifier
.fillMaxSize()
.padding(paddingValues),
Modifier
.fillMaxSize()
.padding(paddingValues),
) {
CourseSelectionSearchScreen(modifier = Modifier.fillMaxSize())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,21 @@ fun CourseSelectionSearchScreen(
) {
SearchBar(
modifier =
Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 8.dp,
),
Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 8.dp,
),
state = searchBarState,
inputField = inputField,
)

CourseSearchResultView(
modifier =
Modifier
.fillMaxWidth()
.weight(1f),
Modifier
.fillMaxWidth()
.weight(1f),
uiState = searchUiState.result,
selectedCourseSerialNumbers = selectedCourseSerialNumbers,
onToggleCourse = planViewModel::toggleCourse,
Expand All @@ -91,9 +91,9 @@ fun CourseSelectionSearchScreen(
) {
CourseSearchResultView(
modifier =
Modifier
.fillMaxWidth()
.weight(1f),
Modifier
.fillMaxWidth()
.weight(1f),
uiState = searchUiState.result,
selectedCourseSerialNumbers = selectedCourseSerialNumbers,
onToggleCourse = planViewModel::toggleCourse,
Expand Down
12 changes: 12 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@ plugins {
alias(libs.plugins.ksp) apply false
alias(libs.plugins.androidx.room) apply false
alias(libs.plugins.detekt) apply false
alias(libs.plugins.ktlint) apply false
alias(libs.plugins.testBalloon) apply false
}

val ktlintCheck by tasks.registering {
group = "verification"
description = "Runs ktlint checks for all Kotlin modules."

dependsOn(
":shared:ktlintCheck",
":androidApp:ktlintCheck",
)
}

val swiftLint by tasks.registering(Exec::class) {
group = "verification"
description = "Runs SwiftLint against the iOS application."
Expand Down Expand Up @@ -55,6 +66,7 @@ tasks.register("check") {
":androidApp:check",
":androidApp:detektDebug",
":androidApp:lintDebug",
ktlintCheck,
swiftLint,
)
}
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ sqlite = "2.7.0"
ksp = "2.3.10"

detekt = "2.0.0-alpha.5"
ktlint = "1.8.0"
ktlintPlugin = "14.2.0"

testBalloon = "1.0.1-K2.4.0"

Expand Down Expand Up @@ -86,3 +88,4 @@ testBalloon = { id = "de.infix.testBalloon", version.ref = "testBalloon" }

ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
detekt = { id = "dev.detekt", version.ref = "detekt" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintPlugin" }
10 changes: 10 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
alias(libs.plugins.testBalloon)
}

Expand Down Expand Up @@ -106,3 +107,12 @@ detekt {
config.setFrom(rootProject.file("config/detekt/detekt.yml"))
buildUponDefaultConfig = true
}

ktlint {
version.set(libs.versions.ktlint)
filter {
exclude {
"/build/generated/" in it.file.invariantSeparatorsPath
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import androidx.datastore.preferences.core.PreferencesSerializer
import okio.FileSystem
import okio.Path.Companion.toOkioPath

fun createDataStore(context: Context): Storage<Preferences> =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = PreferencesSerializer,
producePath = {
context.filesDir
.resolve(DATA_STORE_FILE_NAME)
.toOkioPath()
},
)
fun createDataStore(context: Context): Storage<Preferences> = OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = PreferencesSerializer,
producePath = {
context.filesDir
.resolve(DATA_STORE_FILE_NAME)
.toOkioPath()
},
)
9 changes: 4 additions & 5 deletions shared/src/commonMain/kotlin/org/mpc/core/dataBaseBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import org.mpc.data.local.database.AppDatabase

// This is the database composition root, before the Metro graph exists.
@Suppress("InjectDispatcher")
fun createDatabase(builder: RoomDatabase.Builder<AppDatabase>): AppDatabase =
builder
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
fun createDatabase(builder: RoomDatabase.Builder<AppDatabase>): AppDatabase = builder
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()

internal const val DATABASE_NAME = "mpc.db"
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy

@OptIn(ExperimentalSerializationApi::class)
fun createHttpClient(): HttpClient =
// TODO: maybe assign platform specific engine via expect/actual could be an option
fun createHttpClient(): HttpClient = // TODO: maybe assign platform specific engine via expect/actual could be an option
HttpClient {
install(Logging) {
logger =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ interface CourseCatalogDao {
@Query("DELETE FROM course_catalog WHERE semester = :semester")
suspend fun deleteCatalog(semester: String)

@Query("""
@Query(
"""
DELETE FROM course
WHERE semester = :semester
AND serialNo IN (:serialNos)
""")
""",
)
suspend fun deleteCourses(semester: String, serialNos: List<String>)

@Query("SELECT * FROM course WHERE semester = :semester ORDER BY serialNo")
Expand Down Expand Up @@ -67,7 +69,7 @@ interface CourseCatalogDao {
if (removedCourseSerialNos.isNotEmpty()) {
deleteCourses(
semester = entities.catalog.semester,
serialNos = removedCourseSerialNos
serialNos = removedCourseSerialNos,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ internal fun CourseResult.toLocalCatalogEntities(): LocalCatalogEntities {

return LocalCatalogEntities(
catalog =
CourseCatalogEntity(
semester = semester,
lastUpdatedEpochMillis = lastUpdated.toEpochMilliseconds(),
),
CourseCatalogEntity(
semester = semester,
lastUpdatedEpochMillis = lastUpdated.toEpochMilliseconds(),
),
courses = courseEntities,
teachers = teacherEntities,
courseTimes = timeEntities,
Expand Down Expand Up @@ -82,26 +82,26 @@ internal fun LocalCatalogEntities.toDomain(): CourseResult {
lastUpdated = Instant.fromEpochMilliseconds(catalog.lastUpdatedEpochMillis),
semester = catalog.semester,
courses =
courses.map { course ->
val key =
CourseKey(
semester = course.semester,
serialNo = course.serialNo,
)

course.toDomain(
teachers = teachersByCourse[key].orEmpty().map { it.teacherName },
classTimes =
timesByCourse[key]
.orEmpty()
.mapNotNull { it.toDomainOrNull() }
.sortedWith(
compareBy(
{ it.day.order },
{ it.period.order },
),
),
courses.map { course ->
val key =
CourseKey(
semester = course.semester,
serialNo = course.serialNo,
)
},

course.toDomain(
teachers = teachersByCourse[key].orEmpty().map { it.teacherName },
classTimes =
timesByCourse[key]
.orEmpty()
.mapNotNull { it.toDomainOrNull() }
.sortedWith(
compareBy(
{ it.day.order },
{ it.period.order },
),
),
)
},
)
}
Loading
Loading