+}
+
+sealed interface PositionableComponent {
+ val pos: P
+}
+
+sealed interface PlayerLike : PositionableComponent, StepComponent {
+ val ballState: BallState
+}
+
+data class PlayerComponent(
+ val path: MovementPath?,
+ val team: PlayerTeam,
+ val role: String,
+ override val ballState: BallState,
+ override val pos: FixedPosition,
+ override val id: ComponentId,
+ override val actions: List,
+) : PlayerLike, StepComponent
+
+data class PhantomComponent(
+ val attachedTo: ComponentId?,
+ val originPlayerId: ComponentId,
+ override val ballState: BallState,
+ override val pos: Positioning,
+ override val id: ComponentId,
+ override val actions: List
+) : PlayerLike, StepComponent
+
+data class BallComponent(
+ override val id: ComponentId,
+ override val actions: List,
+ override val pos: FixedPosition
+) : StepComponent, PositionableComponent
diff --git a/app/src/main/java/com/iqball/app/model/tactic/StepContent.kt b/app/src/main/java/com/iqball/app/model/tactic/StepContent.kt
new file mode 100644
index 0000000..968d251
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/model/tactic/StepContent.kt
@@ -0,0 +1,15 @@
+package com.iqball.app.model.tactic
+
+import java.lang.RuntimeException
+
+data class StepContent(val components: List) {
+ inline fun findComponent(id: String): C? {
+ val value = components.find { it.id == id } ?: return null
+ if (!C::class.java.isAssignableFrom(value.javaClass))
+ return null
+ return value as C
+ }
+}
+
+
+class MalformedStepContentException(msg: String, cause: Throwable? = null): RuntimeException(msg, cause)
\ No newline at end of file
diff --git a/app/src/main/java/com/iqball/app/model/tactic/StepNodeInfo.kt b/app/src/main/java/com/iqball/app/model/tactic/StepNodeInfo.kt
new file mode 100644
index 0000000..3c3b7bf
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/model/tactic/StepNodeInfo.kt
@@ -0,0 +1,3 @@
+package com.iqball.app.model.tactic
+
+data class StepNodeInfo(val id: Int, val children: List)
diff --git a/app/src/main/java/com/iqball/app/api/EitherBodyConverter.kt b/app/src/main/java/com/iqball/app/net/EitherBodyConverter.kt
similarity index 97%
rename from app/src/main/java/com/iqball/app/api/EitherBodyConverter.kt
rename to app/src/main/java/com/iqball/app/net/EitherBodyConverter.kt
index 27da66c..8739a96 100644
--- a/app/src/main/java/com/iqball/app/api/EitherBodyConverter.kt
+++ b/app/src/main/java/com/iqball/app/net/EitherBodyConverter.kt
@@ -1,4 +1,4 @@
-package com.iqball.app.api
+package com.iqball.app.net
import arrow.core.Either
import okhttp3.ResponseBody
diff --git a/app/src/main/java/com/iqball/app/api/EitherCall.kt b/app/src/main/java/com/iqball/app/net/EitherCall.kt
similarity index 77%
rename from app/src/main/java/com/iqball/app/api/EitherCall.kt
rename to app/src/main/java/com/iqball/app/net/EitherCall.kt
index 0ceacc9..eb0ce7a 100644
--- a/app/src/main/java/com/iqball/app/api/EitherCall.kt
+++ b/app/src/main/java/com/iqball/app/net/EitherCall.kt
@@ -1,16 +1,16 @@
-package com.iqball.app.api
+package com.iqball.app.net
import arrow.core.Either
-import com.google.gson.Gson
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
+import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
class EitherCall(
- private val gson: Gson,
+ private val retrofit: Retrofit,
private val eitherType: ParameterizedType,
private val delegate: Call
) : Call> {
@@ -21,8 +21,9 @@ class EitherCall(
Either.Right(response.body()!! as R)
} else {
val leftType = eitherType.actualTypeArguments[0]
- val parsed = gson.fromJson(response.errorBody()!!.charStream(), leftType)
- Either.Left(parsed)
+ val converter = retrofit.nextResponseBodyConverter(null, leftType, arrayOf())
+ val result = converter.convert(response.errorBody()!!)!!
+ Either.Left(result)
}
callback.onResponse(this@EitherCall, Response.success(result))
}
@@ -34,7 +35,7 @@ class EitherCall(
})
}
- override fun clone(): Call> = EitherCall(gson, eitherType, delegate.clone())
+ override fun clone(): Call> = EitherCall(retrofit, eitherType, delegate.clone())
override fun execute(): Response> {
throw UnsupportedOperationException()
diff --git a/app/src/main/java/com/iqball/app/api/EitherCallAdapterFactory.kt b/app/src/main/java/com/iqball/app/net/EitherCallAdapterFactory.kt
similarity index 70%
rename from app/src/main/java/com/iqball/app/api/EitherCallAdapterFactory.kt
rename to app/src/main/java/com/iqball/app/net/EitherCallAdapterFactory.kt
index 7d115b4..251f2ac 100644
--- a/app/src/main/java/com/iqball/app/api/EitherCallAdapterFactory.kt
+++ b/app/src/main/java/com/iqball/app/net/EitherCallAdapterFactory.kt
@@ -1,17 +1,13 @@
-package com.iqball.app.api
+package com.iqball.app.net
-import android.os.Build
-import androidx.annotation.RequiresApi
import arrow.core.Either
-import com.google.gson.Gson
-import com.skydoves.retrofit.adapters.arrow.EitherCallAdapterFactory
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
-class EitherCallAdapterFactory(private val gson: Gson) : CallAdapter.Factory() {
+class EitherCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array,
@@ -31,12 +27,12 @@ class EitherCallAdapterFactory(private val gson: Gson) : CallAdapter.Factory() {
override fun responseType(): Type = returnType
override fun adapt(call: Call): EitherCall {
- return EitherCall(gson, eitherType, call)
+ return EitherCall(retrofit, eitherType, call)
}
}
}
companion object {
- fun create(gson: Gson) = EitherCallAdapterFactory(gson)
+ fun create() = EitherCallAdapterFactory()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/iqball/app/api/service/AuthService.kt b/app/src/main/java/com/iqball/app/net/service/AuthService.kt
similarity index 79%
rename from app/src/main/java/com/iqball/app/api/service/AuthService.kt
rename to app/src/main/java/com/iqball/app/net/service/AuthService.kt
index bb444fe..064c8b1 100644
--- a/app/src/main/java/com/iqball/app/api/service/AuthService.kt
+++ b/app/src/main/java/com/iqball/app/net/service/AuthService.kt
@@ -1,11 +1,7 @@
-package com.iqball.app.api.service
+package com.iqball.app.net.service
-import arrow.core.Either
-import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
import retrofit2.http.Body
-import retrofit2.http.GET
-import retrofit2.http.Header
import retrofit2.http.POST
interface AuthService {
diff --git a/app/src/main/java/com/iqball/app/api/service/IQBallService.kt b/app/src/main/java/com/iqball/app/net/service/IQBallService.kt
similarity index 57%
rename from app/src/main/java/com/iqball/app/api/service/IQBallService.kt
rename to app/src/main/java/com/iqball/app/net/service/IQBallService.kt
index bd73875..a80a363 100644
--- a/app/src/main/java/com/iqball/app/api/service/IQBallService.kt
+++ b/app/src/main/java/com/iqball/app/net/service/IQBallService.kt
@@ -1,9 +1,8 @@
-package com.iqball.app.api.service
+package com.iqball.app.net.service
import arrow.core.Either
-import retrofit2.Call
typealias ErrorResponseResult = Map>
typealias APIResult = Either
-interface IQBallService : AuthService, UserService
\ No newline at end of file
+interface IQBallService : AuthService, UserService, TacticService
\ No newline at end of file
diff --git a/app/src/main/java/com/iqball/app/net/service/TacticService.kt b/app/src/main/java/com/iqball/app/net/service/TacticService.kt
new file mode 100644
index 0000000..b6af5df
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/net/service/TacticService.kt
@@ -0,0 +1,44 @@
+package com.iqball.app.net.service
+
+import com.iqball.app.model.tactic.StepContent
+import com.iqball.app.model.tactic.StepNodeInfo
+import com.iqball.app.session.Token
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.Path
+
+interface TacticService {
+
+ data class GetTacticInfoResponse(
+ val id: Int,
+ val name: String,
+ val courtType: String,
+ val creationDate: Long
+ )
+
+ @GET("tactics/{tacticId}")
+ suspend fun getTacticInfo(
+ @Header("Authorization") auth: Token,
+ @Path("tacticId") tacticId: Int
+ ): APIResult
+
+ data class GetTacticStepsTreeResponse(
+ val root: StepNodeInfo
+ )
+
+ @GET("tactics/{tacticId}/tree")
+ suspend fun getTacticStepsTree(
+ @Header("Authorization") auth: Token,
+ @Path("tacticId") tacticId: Int
+ ): APIResult
+
+
+
+ @GET("tactics/{tacticId}/steps/{stepId}")
+ suspend fun getTacticStepContent(
+ @Header("Authorization") auth: Token,
+ @Path("tacticId") tacticId: Int,
+ @Path("stepId") stepId: Int
+ ): APIResult
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/iqball/app/api/service/UserService.kt b/app/src/main/java/com/iqball/app/net/service/UserService.kt
similarity index 88%
rename from app/src/main/java/com/iqball/app/api/service/UserService.kt
rename to app/src/main/java/com/iqball/app/net/service/UserService.kt
index f82376d..2045697 100644
--- a/app/src/main/java/com/iqball/app/api/service/UserService.kt
+++ b/app/src/main/java/com/iqball/app/net/service/UserService.kt
@@ -1,4 +1,4 @@
-package com.iqball.app.api.service
+package com.iqball.app.net.service
import retrofit2.http.GET
import retrofit2.http.Header
diff --git a/app/src/main/java/com/iqball/app/page/HomePage.kt b/app/src/main/java/com/iqball/app/page/HomePage.kt
index 8bcb3bf..94496b5 100644
--- a/app/src/main/java/com/iqball/app/page/HomePage.kt
+++ b/app/src/main/java/com/iqball/app/page/HomePage.kt
@@ -2,7 +2,7 @@ package com.iqball.app.page
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import com.iqball.app.api.service.IQBallService
+import com.iqball.app.net.service.IQBallService
import com.iqball.app.session.Session
@Composable
diff --git a/app/src/main/java/com/iqball/app/page/LoginPage.kt b/app/src/main/java/com/iqball/app/page/LoginPage.kt
index 9c30099..725b448 100644
--- a/app/src/main/java/com/iqball/app/page/LoginPage.kt
+++ b/app/src/main/java/com/iqball/app/page/LoginPage.kt
@@ -15,7 +15,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import arrow.core.Either
-import com.iqball.app.api.service.AuthService
+import com.iqball.app.net.service.AuthService
import com.iqball.app.session.Authentication
import kotlinx.coroutines.runBlocking
diff --git a/app/src/main/java/com/iqball/app/page/RegisterPage.kt b/app/src/main/java/com/iqball/app/page/RegisterPage.kt
index 898ca43..2627018 100644
--- a/app/src/main/java/com/iqball/app/page/RegisterPage.kt
+++ b/app/src/main/java/com/iqball/app/page/RegisterPage.kt
@@ -24,7 +24,9 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import arrow.core.Either
-import com.iqball.app.api.service.AuthService
+
+import com.iqball.app.net.service.AuthService
+import com.iqball.app.net.service.IQBallService
import com.iqball.app.session.Authentication
import kotlinx.coroutines.runBlocking
diff --git a/app/src/main/java/com/iqball/app/page/VisualizerPage.kt b/app/src/main/java/com/iqball/app/page/VisualizerPage.kt
new file mode 100644
index 0000000..ce7bac8
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/page/VisualizerPage.kt
@@ -0,0 +1,158 @@
+package com.iqball.app.page
+
+import android.app.Activity
+import android.content.res.Configuration
+import android.util.Log
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import arrow.core.Either
+import arrow.core.flatMap
+import com.iqball.app.component.BasketCourt
+import com.iqball.app.model.TacticInfo
+import com.iqball.app.model.tactic.CourtType
+import com.iqball.app.model.tactic.StepContent
+import com.iqball.app.model.tactic.StepNodeInfo
+import com.iqball.app.net.service.TacticService
+import com.iqball.app.session.Token
+import kotlinx.coroutines.runBlocking
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+
+private data class VisualizerInitialData(
+ val info: TacticInfo,
+ val rootStep: StepNodeInfo,
+ val rootContent: StepContent
+)
+
+@Composable
+fun VisualizerPage(
+ service: TacticService,
+ auth: Token,
+ tacticId: Int,
+) {
+ val dataEither = initializeVisualizer(service, auth, tacticId)
+
+ val (info, rootStep, rootContent) = when (dataEither) {
+ is Either.Left -> return Text(text = dataEither.value)
+ is Either.Right -> dataEither.value
+ }
+
+ val screenOrientation = LocalConfiguration.current.orientation
+
+ Column {
+ VisualizerHeader(title = info.name)
+ when (screenOrientation) {
+ Configuration.ORIENTATION_PORTRAIT -> Text(
+ text = "Visualizing Tactic Steps tree",
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ )
+
+ Configuration.ORIENTATION_LANDSCAPE -> BasketCourt(
+ content = rootContent,
+ type = info.type
+ )
+
+ else -> throw Exception("Could not determine device's orientation.")
+ }
+ }
+
+}
+
+@Composable
+private fun VisualizerHeader(title: String) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ IconButton(
+ onClick = { /*TODO*/ }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = Color.Black
+ )
+ }
+
+ Text(text = title, color = Color.Black)
+
+ Text(text = "")
+ }
+}
+
+private fun initializeVisualizer(
+ service: TacticService,
+ auth: Token,
+ tacticId: Int
+): Either {
+ val (tacticInfo, tacticTree, rootStepContent) = runBlocking {
+ val tacticInfo = service.getTacticInfo(auth, tacticId)
+ .map {
+ TacticInfo(
+ id = it.id,
+ name = it.name,
+ type = CourtType.valueOf(
+ it.courtType.lowercase().replaceFirstChar(Char::uppercaseChar)
+ ),
+ creationDate = LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(it.creationDate),
+ ZoneId.systemDefault()
+ )
+ )
+ }.onLeft {
+ Log.e(
+ "received error response from server when retrieving tacticInfo : {}",
+ it.toString()
+ )
+ }
+
+ val tacticTree = service.getTacticStepsTree(auth, tacticId)
+ .map { it.root }
+ .onLeft {
+ Log.e(
+ "received error response from server when retrieving tactic steps tree: {}",
+ it.toString()
+ )
+ }
+
+ val rootStepContent = tacticTree
+ .flatMap {
+ service.getTacticStepContent(auth, tacticId, it.id)
+ .onLeft {
+ Log.e(
+ "received error response from server when retrieving root content: {}",
+ it.toString()
+ )
+ }
+ }
+
+ Triple(tacticInfo.getOrNull(), tacticTree.getOrNull(), rootStepContent.getOrNull())
+ }
+
+ if (tacticInfo == null || tacticTree == null || rootStepContent == null) {
+ return Either.Left("Unable to retrieve tactic information")
+ }
+
+
+ return Either.Right(VisualizerInitialData(tacticInfo, tacticTree, rootStepContent))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt b/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt
new file mode 100644
index 0000000..0ad4d36
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt
@@ -0,0 +1,74 @@
+package com.iqball.app.serialization
+
+import arrow.core.Either
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.rawType
+import java.lang.ClassCastException
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+
+
+private class EitherTypeAdapter(val leftType: JsonPrimitiveType, val rightType: JsonPrimitiveType) :
+ JsonAdapter>() {
+ override fun fromJson(reader: JsonReader): Either<*, *>? {
+ val value = reader.readJsonValue() ?: return null
+
+ val valueJsonType = value.javaClass.getJsonPrimitive()
+ if (valueJsonType == leftType)
+ return Either.Left(value)
+
+ if (valueJsonType == rightType)
+ return Either.Right(value)
+ throw ClassCastException("Cannot cast a value of type " + value.javaClass + " as either " + leftType.name.lowercase() + " or " + rightType.name.lowercase())
+ }
+
+
+ override fun toJson(writer: JsonWriter, value: Either<*, *>?) {
+ when (value) {
+ is Either.Left -> writer.jsonValue(value.value)
+ is Either.Right -> writer.jsonValue(value.value)
+ null -> writer.nullValue()
+ }
+ }
+
+
+}
+
+object EitherTypeAdapterFactory: JsonAdapter.Factory {
+ override fun create(
+ type: Type,
+ annotations: MutableSet,
+ moshi: Moshi
+ ): JsonAdapter<*>? {
+ if (type !is ParameterizedType)
+ return null
+ if (type.rawType != Either::class.java)
+ return null
+ val leftType = type.actualTypeArguments[0].rawType.getJsonPrimitive()
+ val rightType = type.actualTypeArguments[1].rawType.getJsonPrimitive()
+ if (leftType == rightType) {
+ throw UnsupportedOperationException("Cannot handle Either types with both sides being object, array, string or number. Provided type is : $type")
+ }
+ return EitherTypeAdapter(leftType, rightType)
+ }
+}
+
+private enum class JsonPrimitiveType {
+ Array,
+ Object,
+ String,
+ Number;
+}
+
+private fun Class<*>.getJsonPrimitive(): JsonPrimitiveType {
+ if (isPrimitive)
+ return JsonPrimitiveType.Number
+ if (isArray)
+ return JsonPrimitiveType.Array
+ if (this == String::class.java)
+ return JsonPrimitiveType.String
+ return JsonPrimitiveType.Object
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/iqball/app/serialization/EnumTypeAdapter.kt b/app/src/main/java/com/iqball/app/serialization/EnumTypeAdapter.kt
new file mode 100644
index 0000000..01e76b8
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/serialization/EnumTypeAdapter.kt
@@ -0,0 +1,81 @@
+package com.iqball.app.serialization
+
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.rawType
+import java.lang.reflect.Type
+import java.util.NoSuchElementException
+
+class EnumTypeAdapter>(
+ values: Map,
+ private val bindNames: Boolean,
+ private val ignoreCase: Boolean,
+ private val fallback: E?,
+ private val clazz: Class
+) : JsonAdapter() {
+
+ private val values = if (ignoreCase) values.mapKeys { it.key.lowercase() } else values
+
+ override fun fromJson(reader: JsonReader): E {
+ val value = reader.nextString()
+ val key = if (ignoreCase) value.lowercase() else value
+
+ var result = values[key]
+ if (result == null && bindNames) {
+ result = clazz.enumConstants?.find { it.name.lowercase() == key }
+ }
+
+ return result
+ ?: fallback
+ ?: throw NoSuchElementException("No enum variant matched given values bindings, and no fallback was provided (value = $value, enum type = $clazz)")
+ }
+
+ override fun toJson(writer: JsonWriter, value: E?) {
+ throw UnsupportedOperationException()
+ }
+
+}
+
+class EnumTypeAdapterFactory>(
+ private val values: Map,
+ private val bindNames: Boolean,
+ private val ignoreCase: Boolean,
+ private val fallback: E?,
+ private val clazz: Class
+) : JsonAdapter.Factory {
+ override fun create(
+ type: Type,
+ annotations: MutableSet,
+ moshi: Moshi
+ ): JsonAdapter<*>? {
+ if (type.rawType != clazz)
+ return null
+
+ return EnumTypeAdapter(values, bindNames, ignoreCase, fallback, clazz)
+ }
+
+
+ companion object {
+
+ class Builder>(val values: MutableMap = HashMap()) {
+ infix fun String.means(e: E) {
+ values[this] = e
+ }
+ }
+
+ inline fun > create(
+ ignoreCase: Boolean = false,
+ bindNames: Boolean = true,
+ fallback: E? = null,
+ build: Builder.() -> Unit = {}
+ ): EnumTypeAdapterFactory {
+ val builder = Builder()
+ build(builder)
+ return EnumTypeAdapterFactory(builder.values, bindNames, ignoreCase, fallback, E::class.java)
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/iqball/app/session/Authentication.kt b/app/src/main/java/com/iqball/app/session/Authentication.kt
index b5dbeaf..ff2855f 100644
--- a/app/src/main/java/com/iqball/app/session/Authentication.kt
+++ b/app/src/main/java/com/iqball/app/session/Authentication.kt
@@ -1,3 +1,4 @@
package com.iqball.app.session
+typealias Token = String
data class Authentication(val token: String, val expirationDate: Long)
diff --git a/app/src/main/java/com/iqball/app/ui/theme/Color.kt b/app/src/main/java/com/iqball/app/ui/theme/Color.kt
index 6b01d6c..9f29911 100644
--- a/app/src/main/java/com/iqball/app/ui/theme/Color.kt
+++ b/app/src/main/java/com/iqball/app/ui/theme/Color.kt
@@ -8,4 +8,8 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+val Pink40 = Color(0xFF7D5260)
+
+val Allies = Color(0xFF64e4f5)
+val Opponents = Color(0xFFf59264)
+val BallColor = Color(0XFFc5520d)
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ball.xml b/app/src/main/res/drawable/ball.xml
new file mode 100644
index 0000000..5b55cb0
--- /dev/null
+++ b/app/src/main/res/drawable/ball.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/half_court.xml b/app/src/main/res/drawable/half_court.xml
new file mode 100644
index 0000000..d84c400
--- /dev/null
+++ b/app/src/main/res/drawable/half_court.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/plain_court.xml b/app/src/main/res/drawable/plain_court.xml
new file mode 100644
index 0000000..8ae62a7
--- /dev/null
+++ b/app/src/main/res/drawable/plain_court.xml
@@ -0,0 +1,301 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/tree_icon.png b/app/src/main/res/drawable/tree_icon.png
new file mode 100644
index 0000000..221099c
Binary files /dev/null and b/app/src/main/res/drawable/tree_icon.png differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 395069b..412a487 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,6 +2,8 @@
agp = "8.2.2"
arrowCore = "1.2.1"
converterGson = "2.9.0"
+converterMoshi = "2.4.0"
+converterMoshiVersion = "2.5.0"
kotlin = "1.9.0"
coreKtx = "1.10.1"
junit = "4.13.2"
@@ -12,15 +14,21 @@ kotlinxSerializationJsonJvm = "1.6.3"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.7.0"
composeBom = "2023.08.00"
+moshi = "1.15.1"
+moshiAdapters = "1.15.1"
+moshiKotlin = "1.15.1"
retrofit = "2.9.0"
retrofit2KotlinxSerializationConverter = "1.0.0"
retrofitAdaptersArrow = "1.0.9"
navigationCompose = "2.7.7"
+zoomable = "1.6.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
+converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" }
+converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -36,10 +44,14 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlinxSerializationJsonJvm" }
+moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
+moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshiAdapters" }
+moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-adapters-arrow = { module = "com.github.skydoves:retrofit-adapters-arrow", version.ref = "retrofitAdaptersArrow" }
retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }