+}
+
+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..f91931d
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/model/tactic/StepNodeInfo.kt
@@ -0,0 +1,17 @@
+package com.iqball.app.model.tactic
+
+data class StepNodeInfo(val id: Int, val children: List)
+
+
+fun getParent(root: StepNodeInfo, child: Int): StepNodeInfo? {
+ for (children in root.children) {
+ if (children.id == child) {
+ return root
+ }
+ val result = getParent(children, child)
+ if (result != null)
+ return result
+ }
+
+ return null
+}
\ No newline at end of file
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..4ee3f98 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,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/VisualizerPage.kt b/app/src/main/java/com/iqball/app/page/VisualizerPage.kt
new file mode 100644
index 0000000..f85787a
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/page/VisualizerPage.kt
@@ -0,0 +1,213 @@
+package com.iqball.app.page
+
+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.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+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.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateMapOf
+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.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.zIndex
+import arrow.core.Either
+import com.iqball.app.R
+import com.iqball.app.component.BasketCourt
+import com.iqball.app.component.BasketCourtStates
+import com.iqball.app.component.StepsTree
+import com.iqball.app.model.TacticInfo
+import com.iqball.app.model.tactic.ComponentId
+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.model.tactic.getParent
+import com.iqball.app.net.service.TacticService
+import com.iqball.app.session.Token
+import kotlinx.coroutines.runBlocking
+import net.engawapg.lib.zoomable.ZoomState
+import net.engawapg.lib.zoomable.rememberZoomState
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+
+private data class VisualizerInitialData(
+ val info: TacticInfo,
+ val rootStep: StepNodeInfo,
+)
+
+@Composable
+fun VisualizerPage(
+ service: TacticService,
+ auth: Token,
+ tacticId: Int,
+) {
+ val dataEither = remember { initializeVisualizer(service, auth, tacticId) }
+ val showTree = remember { mutableStateOf(true) }
+
+ val (info, stepsTree) = when (dataEither) {
+ // On error return a text to print it to the user
+ is Either.Left -> return Text(text = dataEither.value)
+ is Either.Right -> dataEither.value
+ }
+
+ fun getStepContent(step: Int): StepContent = runBlocking {
+ val result = service.getTacticStepContent(auth, tacticId, step).onLeft {
+ Log.e(
+ "received error response from server when retrieving step content: {}",
+ it.toString()
+ )
+ }
+ when (result) {
+ is Either.Left -> throw Error("Unexpected error")
+ is Either.Right -> result.value
+ }
+ }
+
+
+ val screenOrientation = LocalConfiguration.current.orientation
+ var selectedStepId by rememberSaveable { mutableIntStateOf(stepsTree.id) }
+ val (content, parentContent) = remember(selectedStepId) {
+ val parentId = getParent(stepsTree, selectedStepId)?.id
+ Pair(
+ getStepContent(selectedStepId),
+ parentId?.let { getStepContent(it) }
+ )
+ }
+
+
+ Column {
+ VisualizerHeader(title = info.name, showTree)
+ when (screenOrientation) {
+ Configuration.ORIENTATION_PORTRAIT -> StepsTree(root = stepsTree,
+ selectedNodeId = selectedStepId,
+ onNodeSelected = { selectedStepId = it.id })
+
+ Configuration.ORIENTATION_LANDSCAPE -> {
+ val courtArea = remember { mutableStateOf(Rect.Zero) }
+ val stepOffsets =
+ remember(selectedStepId) { mutableStateMapOf() }
+ val parentOffsets =
+ remember(selectedStepId) { mutableStateMapOf() }
+
+ val courtModifier =
+ if (showTree.value) Modifier.width(IntrinsicSize.Min) else Modifier.fillMaxWidth()
+
+
+ val courtZoomState = remember { ZoomState() }
+
+ Row(modifier = Modifier.background(Color.LightGray)) {
+ BasketCourt(
+ content = content,
+ parentContent,
+ type = info.type,
+ modifier = courtModifier,
+ state = BasketCourtStates(
+ stepOffsets,
+ parentOffsets,
+ courtArea,
+ courtZoomState
+ )
+ )
+ if (showTree.value) {
+ StepsTree(
+ root = stepsTree,
+ selectedNodeId = selectedStepId,
+ onNodeSelected = { selectedStepId = it.id }
+ )
+ }
+ }
+ }
+
+ else -> throw Exception("Could not determine device's orientation.")
+ }
+ }
+
+}
+
+@Composable
+private fun VisualizerHeader(title: String, showTree: MutableState) {
+
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .zIndex(10000F)
+ .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)
+
+ IconButton(onClick = { showTree.value = !showTree.value }) {
+ Icon(
+ painter = painterResource(id = R.drawable.tree_icon),
+ contentDescription = "toggle show tree"
+ )
+ }
+ }
+}
+
+private fun initializeVisualizer(
+ service: TacticService, auth: Token, tacticId: Int
+): Either {
+ val (tacticInfo, tacticTree) = 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()
+ )
+ }
+
+ Pair(tacticInfo.getOrNull(), tacticTree.getOrNull())
+ }
+
+ if (tacticInfo == null || tacticTree == null) {
+ return Either.Left("Unable to retrieve tactic information")
+ }
+
+
+ return Either.Right(VisualizerInitialData(tacticInfo, tacticTree))
+}
\ 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..b09376a
--- /dev/null
+++ b/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt
@@ -0,0 +1,97 @@
+package com.iqball.app.serialization
+
+import arrow.core.Either
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonDataException
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.rawType
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+
+
+private class EitherTypeAdapter(
+ val leftType: Class<*>,
+ val rightType: Class<*>,
+ private val moshi: Moshi
+) : JsonAdapter>() {
+
+ private val leftJsonType = leftType.getJsonPrimitive()
+ private val rightJsonType = rightType.getJsonPrimitive()
+
+ override fun fromJson(reader: JsonReader): Either<*, *>? {
+
+ val valueJsonType = when (val token = reader.peek()) {
+ JsonReader.Token.BEGIN_ARRAY -> JsonPrimitiveType.Array
+ JsonReader.Token.BEGIN_OBJECT -> JsonPrimitiveType.Object
+ JsonReader.Token.STRING -> JsonPrimitiveType.String
+ JsonReader.Token.NUMBER -> JsonPrimitiveType.Number
+ JsonReader.Token.BOOLEAN -> JsonPrimitiveType.Boolean
+ JsonReader.Token.NULL -> {
+ reader.nextNull()
+ return null
+ }
+ else -> throw JsonDataException("unexpected token : $token")
+ }
+
+ if (valueJsonType == leftJsonType) {
+ val value = moshi.adapter(leftType).fromJson(reader)
+ return Either.Left(value)
+ }
+
+ if (valueJsonType == rightJsonType) {
+ val value = moshi.adapter(rightType).fromJson(reader)
+ return Either.Right(value)
+ }
+ throw ClassCastException("Cannot cast a json value of type " + valueJsonType + " 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
+ val rightType = type.actualTypeArguments[1].rawType
+ if (leftType.getJsonPrimitive() == rightType.getJsonPrimitive()) {
+ throw UnsupportedOperationException("Cannot handle Either types with both sides being object, array, string or number. Provided type is : $type")
+ }
+ return EitherTypeAdapter(leftType, rightType, moshi)
+ }
+}
+
+private enum class JsonPrimitiveType {
+ Array,
+ Object,
+ String,
+ Number,
+ Boolean
+}
+
+private fun Class<*>.getJsonPrimitive(): JsonPrimitiveType {
+ if (isPrimitive)
+ return if (java.lang.Boolean.TYPE == this) JsonPrimitiveType.Boolean else 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..17833f0 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,11 @@ 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)
+
+val StepNode = Color(0xFF2AC008)
+val SelectedStepNode = Color(0xFF213519)
\ 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..1f7e07f
--- /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..ca4c795 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,26 +1,36 @@
[versions]
agp = "8.2.2"
arrowCore = "1.2.1"
+composeFreeScroll = "0.2.2"
converterGson = "2.9.0"
+converterMoshi = "2.5.0"
+converterMoshiVersion = "2.5.0"
+graphicsShapes = "1.0.0-alpha05"
kotlin = "1.9.0"
-coreKtx = "1.10.1"
+coreKtx = "1.12.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
kotlinxDatetime = "0.3.2"
kotlinxSerializationJsonJvm = "1.6.3"
-lifecycleRuntimeKtx = "2.6.1"
-activityCompose = "1.7.0"
-composeBom = "2023.08.00"
+lifecycleRuntimeKtx = "2.7.0"
+activityCompose = "1.8.2"
+composeBom = "2024.02.02"
+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" }
+androidx-graphics-shapes = { module = "androidx.graphics:graphics-shapes", version.ref = "graphicsShapes" }
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" }
-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
+compose-free-scroll = { module = "com.github.chihsuanwu:compose-free-scroll", version.ref = "composeFreeScroll" }
+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" }
@@ -35,11 +45,14 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
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" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ef8f906..7071bcf 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,5 @@
+import java.net.URI
+
pluginManagement {
repositories {
google {
@@ -16,6 +18,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven { url = URI.create("https://jitpack.io") }
}
}