From 4dd63b7d1df84e3e0e182f4edee3bc275ca63f0f Mon Sep 17 00:00:00 2001 From: LavaDesu Date: Mon, 14 Apr 2025 21:07:05 +1000 Subject: [PATCH] feat: initial api support --- .gitignore | 1 + composeApp/build.gradle.kts | 3 + .../src/androidMain/AndroidManifest.xml | 2 + .../commonMain/kotlin/moe/lava/banksia/App.kt | 3 + .../kotlin/moe/lava/banksia/ui/Searcher.kt | 47 ++++++++++----- gradle/libs.versions.toml | 15 ++++- .../kotlin/moe/lava/banksia/Application.kt | 4 +- shared/build.gradle.kts | 15 ++++- .../moe/lava/banksia/Logging.android.kt | 7 +++ .../moe/lava/banksia/Platform.android.kt | 9 --- .../kotlin/moe/lava/banksia/Constants.kt | 3 - .../moe/lava/banksia/Constants.kt.skeleton | 6 ++ .../kotlin/moe/lava/banksia/Greeting.kt | 9 --- .../kotlin/moe/lava/banksia/Logging.kt | 3 + .../kotlin/moe/lava/banksia/Platform.kt | 7 --- .../moe/lava/banksia/api/ptv/PtvService.kt | 58 +++++++++++++++++++ .../kotlin/moe/lava/banksia/Logging.ios.kt | 5 ++ .../kotlin/moe/lava/banksia/Platform.ios.kt | 9 --- .../kotlin/moe/lava/banksia/Logging.jvm.kt | 5 ++ .../kotlin/moe/lava/banksia/Platform.jvm.kt | 7 --- 20 files changed, 156 insertions(+), 62 deletions(-) create mode 100644 shared/src/androidMain/kotlin/moe/lava/banksia/Logging.android.kt delete mode 100644 shared/src/androidMain/kotlin/moe/lava/banksia/Platform.android.kt delete mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton delete mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/Greeting.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/Logging.kt delete mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/Platform.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt create mode 100644 shared/src/iosMain/kotlin/moe/lava/banksia/Logging.ios.kt delete mode 100644 shared/src/iosMain/kotlin/moe/lava/banksia/Platform.ios.kt create mode 100644 shared/src/jvmMain/kotlin/moe/lava/banksia/Logging.jvm.kt delete mode 100644 shared/src/jvmMain/kotlin/moe/lava/banksia/Platform.jvm.kt diff --git a/.gitignore b/.gitignore index 3bc06b7..8f39b51 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ captures **/xcshareddata/WorkspaceSettings.xcsettings secrets.properties +shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index d06db07..9ef224c 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -34,6 +34,7 @@ kotlin { androidMain.dependencies { implementation(compose.preview) implementation(libs.androidx.activity.compose) + implementation(libs.kotlinx.coroutines.android) implementation(libs.play.services.maps) implementation(libs.maps.compose) } @@ -47,7 +48,9 @@ kotlin { implementation(compose.components.uiToolingPreview) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.kotlinx.coroutines.core) implementation(projects.shared) + } } } diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index c6c63da..2b2ead7 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,12 +1,14 @@ + Unit, text: String, onTextChange: (String) -> Unit, + onRouteChange: (Route) -> Unit, ) { val animatedPadding by animateDpAsState( if (expanded) { @@ -39,9 +52,17 @@ fun Searcher( }, label = "padding" ) + var routes by rememberSaveable { mutableStateOf(listOf()) } Box(modifier = Modifier.fillMaxSize()) { LaunchedEffect(Unit) { - /*cache.routes()*/ + val localRoutes = ptvService.routes() + routes = localRoutes.sortedWith( + compareBy( +// { it.routeType.ordinal }, + { it.routeNumber.toIntOrNull() }, + { it.routeName } + ) + ) } SearchBar( colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainer), @@ -71,33 +92,31 @@ fun Searcher( onExpandedChange = onExpandedChange, ) { LazyColumn(modifier = Modifier.fillMaxWidth()) { - /*val r = cache.sortedRoutes() - for ((_, route) in r) { - if (!route.route_number.contains(text) && - !route.route_name.lowercase().contains(text.lowercase())) + for (route in routes) { + if (!route.routeNumber.contains(text) && + !route.routeName.lowercase().contains(text.lowercase())) continue item { ListItem( - headlineContent = { Text(route.route_number.ifEmpty { route.route_name }) }, + headlineContent = { Text(route.routeNumber.ifEmpty { route.routeName }) }, supportingContent = { - if (route.route_number.isNotEmpty()) { - Text(route.route_name) + if (route.routeNumber.isNotEmpty()) { + Text(route.routeName) } }, - leadingContent = { route.route_type.ComposableIcon() }, +// leadingContent = { route.route_type.ComposableIcon() }, colors = ListItemDefaults.colors(containerColor = Color.Transparent), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp) .clickable { - text = "${route.route_number} - ${route.route_name}" - - onRouteChanged(route) - expanded = false + onTextChange("${route.routeNumber} - ${route.routeName}") + onExpandedChange(false) + onRouteChange(route) } ) } - }*/ + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2fe6191..56e6637 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,11 +12,14 @@ androidx-lifecycle = "2.8.4" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" compose-multiplatform = "1.7.3" +coroutines = "1.9.0" junit = "4.13.2" kotlin = "2.1.10" +kotlinxSerializationJson = "1.8.1" ktor = "3.1.1" logback = "1.5.17" mapsCompose = "6.4.1" +okio = "3.11.0" playServicesMaps = "19.1.0" secretsGradlePlugin = "2.0.1" @@ -33,11 +36,20 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } -logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } +logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" } +okio = { module = "com.squareup.okio:okio", version.ref = "okio" } play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } @@ -49,4 +61,5 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } ktor = { id = "io.ktor.plugin", version.ref = "ktor" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } secretsGradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" } \ No newline at end of file diff --git a/server/src/main/kotlin/moe/lava/banksia/Application.kt b/server/src/main/kotlin/moe/lava/banksia/Application.kt index de70634..819f11c 100644 --- a/server/src/main/kotlin/moe/lava/banksia/Application.kt +++ b/server/src/main/kotlin/moe/lava/banksia/Application.kt @@ -7,14 +7,14 @@ import io.ktor.server.response.* import io.ktor.server.routing.* fun main() { - embeddedServer(Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module) + embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) .start(wait = true) } fun Application.module() { routing { get("/") { - call.respondText("Ktor: ${Greeting().greet()}") + call.respondText("Ktor: Hi") } } } \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index d92552b..7bb1c63 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) alias(libs.plugins.androidLibrary) } @@ -19,10 +20,22 @@ kotlin { iosSimulatorArm64() jvm() - + sourceSets { + androidMain.dependencies { + implementation(libs.ktor.client.okhttp) + } commonMain.dependencies { + implementation(libs.okio) // put your Multiplatform dependencies here + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) } } } diff --git a/shared/src/androidMain/kotlin/moe/lava/banksia/Logging.android.kt b/shared/src/androidMain/kotlin/moe/lava/banksia/Logging.android.kt new file mode 100644 index 0000000..a8797d8 --- /dev/null +++ b/shared/src/androidMain/kotlin/moe/lava/banksia/Logging.android.kt @@ -0,0 +1,7 @@ +package moe.lava.banksia + +import android.util.Log + +actual fun log(tag: String, msg: String) { + Log.i(tag, msg) +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/moe/lava/banksia/Platform.android.kt b/shared/src/androidMain/kotlin/moe/lava/banksia/Platform.android.kt deleted file mode 100644 index 25abca8..0000000 --- a/shared/src/androidMain/kotlin/moe/lava/banksia/Platform.android.kt +++ /dev/null @@ -1,9 +0,0 @@ -package moe.lava.banksia - -import android.os.Build - -class AndroidPlatform : Platform { - override val name: String = "Android ${Build.VERSION.SDK_INT}" -} - -actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt deleted file mode 100644 index 0467849..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt +++ /dev/null @@ -1,3 +0,0 @@ -package moe.lava.banksia - -const val SERVER_PORT = 8080 \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton b/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton new file mode 100644 index 0000000..39bb7cf --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton @@ -0,0 +1,6 @@ +package moe.lava.banksia + +object Constants { + const val devid: String = "" + const val key: String = "" +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/Greeting.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/Greeting.kt deleted file mode 100644 index 3679ab9..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/Greeting.kt +++ /dev/null @@ -1,9 +0,0 @@ -package moe.lava.banksia - -class Greeting { - private val platform = getPlatform() - - fun greet(): String { - return "Hello, ${platform.name}!" - } -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/Logging.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/Logging.kt new file mode 100644 index 0000000..8eb39eb --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/Logging.kt @@ -0,0 +1,3 @@ +package moe.lava.banksia + +expect fun log(tag: String, msg: String) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/Platform.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/Platform.kt deleted file mode 100644 index 93d6cbf..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/Platform.kt +++ /dev/null @@ -1,7 +0,0 @@ -package moe.lava.banksia - -interface Platform { - val name: String -} - -expect fun getPlatform(): Platform \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt new file mode 100644 index 0000000..3c4a188 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt @@ -0,0 +1,58 @@ +package moe.lava.banksia.api.ptv + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.plugin +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.http.encodedPath +import io.ktor.serialization.kotlinx.json.json +import kotlinx.io.bytestring.encodeToByteString +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import moe.lava.banksia.Constants +import moe.lava.banksia.log +import okio.ByteString.Companion.encodeUtf8 + +@Serializable +data class Route( + @SerialName("route_id") val routeId: Int, + @SerialName("route_number") val routeNumber: String, + @SerialName("route_name") val routeName: String, +) + +@Serializable +data class RouteResponse(val routes: List) + +class PtvService { + private val client = HttpClient() { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + }) + } + defaultRequest { + url("https://timetableapi.ptv.vic.gov.au/v3/") + } + } + + constructor() { + client.plugin(HttpSend).intercept { req -> + req.parameter("devid", Constants.devid) + val fullPath = req.url.build().encodedPathAndQuery + val hash = fullPath.encodeUtf8().hmacSha1(Constants.key.encodeUtf8()).hex() + req.parameter("signature", hash) + log("ktor.intercept", req.url.build().encodedPathAndQuery) + execute(req) + } + } + + suspend fun routes(): List { + val response: RouteResponse = client.get("routes").body() + return response.routes + } +} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/moe/lava/banksia/Logging.ios.kt b/shared/src/iosMain/kotlin/moe/lava/banksia/Logging.ios.kt new file mode 100644 index 0000000..5053352 --- /dev/null +++ b/shared/src/iosMain/kotlin/moe/lava/banksia/Logging.ios.kt @@ -0,0 +1,5 @@ +package moe.lava.banksia + +actual fun log(tag: String, msg: String) { + TODO("Not yet implemented") +} diff --git a/shared/src/iosMain/kotlin/moe/lava/banksia/Platform.ios.kt b/shared/src/iosMain/kotlin/moe/lava/banksia/Platform.ios.kt deleted file mode 100644 index b192024..0000000 --- a/shared/src/iosMain/kotlin/moe/lava/banksia/Platform.ios.kt +++ /dev/null @@ -1,9 +0,0 @@ -package moe.lava.banksia - -import platform.UIKit.UIDevice - -class IOSPlatform: Platform { - override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion -} - -actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file diff --git a/shared/src/jvmMain/kotlin/moe/lava/banksia/Logging.jvm.kt b/shared/src/jvmMain/kotlin/moe/lava/banksia/Logging.jvm.kt new file mode 100644 index 0000000..602e58b --- /dev/null +++ b/shared/src/jvmMain/kotlin/moe/lava/banksia/Logging.jvm.kt @@ -0,0 +1,5 @@ +package moe.lava.banksia + +actual fun log(tag: String, msg: String) { + println("[$tag] $msg") +} \ No newline at end of file diff --git a/shared/src/jvmMain/kotlin/moe/lava/banksia/Platform.jvm.kt b/shared/src/jvmMain/kotlin/moe/lava/banksia/Platform.jvm.kt deleted file mode 100644 index 697bfc2..0000000 --- a/shared/src/jvmMain/kotlin/moe/lava/banksia/Platform.jvm.kt +++ /dev/null @@ -1,7 +0,0 @@ -package moe.lava.banksia - -class JVMPlatform: Platform { - override val name: String = "Java ${System.getProperty("java.version")}" -} - -actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file