feat: initial api support

This commit is contained in:
LavaDesu 2025-04-14 21:07:05 +10:00
parent ad50e700d4
commit 4dd63b7d1d
Signed by: cilly
GPG key ID: 6500251E087653C9
20 changed files with 156 additions and 62 deletions

1
.gitignore vendored
View file

@ -18,3 +18,4 @@ captures
**/xcshareddata/WorkspaceSettings.xcsettings
secrets.properties
shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt

View file

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

View file

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<meta-data
android:name="com.google.android.geo.API_KEY"

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import moe.lava.banksia.api.ptv.PtvService
import moe.lava.banksia.native.maps.Maps
import moe.lava.banksia.ui.Searcher
import org.jetbrains.compose.ui.tooling.preview.Preview
@ -42,10 +43,12 @@ fun App() {
sheetState = scaffoldState.bottomSheetState,
)
Searcher(
ptvService = PtvService(),
expanded = searchExpandedState,
onExpandedChange = { searchExpandedState = it },
text = searchTextState,
onTextChange = { searchTextState = it },
onRouteChange = {}
)
}
}

View file

@ -12,24 +12,37 @@ import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
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.unit.dp
import moe.lava.banksia.api.ptv.PtvService
import moe.lava.banksia.api.ptv.Route
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Searcher(
ptvService: PtvService,
expanded: Boolean,
onExpandedChange: (Boolean) -> 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<Route>()) }
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)
}
)
}
}*/
}
}
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
package moe.lava.banksia
import android.util.Log
actual fun log(tag: String, msg: String) {
Log.i(tag, msg)
}

View file

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

View file

@ -1,3 +0,0 @@
package moe.lava.banksia
const val SERVER_PORT = 8080

View file

@ -0,0 +1,6 @@
package moe.lava.banksia
object Constants {
const val devid: String = ""
const val key: String = ""
}

View file

@ -1,9 +0,0 @@
package moe.lava.banksia
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}

View file

@ -0,0 +1,3 @@
package moe.lava.banksia
expect fun log(tag: String, msg: String)

View file

@ -1,7 +0,0 @@
package moe.lava.banksia
interface Platform {
val name: String
}
expect fun getPlatform(): Platform

View file

@ -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<Route>)
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<Route> {
val response: RouteResponse = client.get("routes").body()
return response.routes
}
}

View file

@ -0,0 +1,5 @@
package moe.lava.banksia
actual fun log(tag: String, msg: String) {
TODO("Not yet implemented")
}

View file

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

View file

@ -0,0 +1,5 @@
package moe.lava.banksia
actual fun log(tag: String, msg: String) {
println("[$tag] $msg")
}

View file

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