Steps tree is now displayed alongside the visualizer

maxime.batista 1 year ago
parent a9d6e20eaf
commit 53d3254f7b

@ -67,7 +67,7 @@ class MainActivity : ComponentActivity() {
.addConverterFactory(EitherBodyConverter.create()) .addConverterFactory(EitherBodyConverter.create())
.addConverterFactory(MoshiConverterFactory.create(moshi)) .addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(EitherCallAdapterFactory.create()) .addCallAdapterFactory(EitherCallAdapterFactory.create())
.baseUrl("http://grospc:5254/") .baseUrl("http://192.168.127.83:5254/")
.client( .client(
OkHttpClient.Builder() OkHttpClient.Builder()
.addInterceptor { it.proceed(it.request()) } .addInterceptor { it.proceed(it.request()) }
@ -98,5 +98,5 @@ fun App(service: IQBallService) {
auth.getOrNull()!!.token auth.getOrNull()!!.token
} }
VisualizerPage(service, auth, 21) VisualizerPage(service, auth, 3)
} }

@ -3,14 +3,10 @@ package com.iqball.app.component
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -31,42 +27,51 @@ import com.iqball.app.model.tactic.ComponentId
import com.iqball.app.model.tactic.CourtType import com.iqball.app.model.tactic.CourtType
import com.iqball.app.model.tactic.PlayerLike import com.iqball.app.model.tactic.PlayerLike
import com.iqball.app.model.tactic.StepContent import com.iqball.app.model.tactic.StepContent
import net.engawapg.lib.zoomable.ZoomState
import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
data class BasketCourtStates(
val componentsOffsets: MutableMap<ComponentId, Offset>,
val courtArea: MutableState<Rect>,
val zoomState: ZoomState
)
@Composable @Composable
fun BasketCourt(content: StepContent, type: CourtType) { fun BasketCourt(
content: StepContent,
type: CourtType,
modifier: Modifier,
state: BasketCourtStates
) {
val courtImg = when (type) { val courtImg = when (type) {
CourtType.Plain -> R.drawable.plain_court CourtType.Plain -> R.drawable.plain_court
CourtType.Half -> R.drawable.half_court CourtType.Half -> R.drawable.half_court
} }
val zoomState = rememberZoomState() val zoomState = state.zoomState
val components = content.components val components = content.components
val componentsOffset = remember { mutableStateMapOf<ComponentId, Offset>() } val componentsOffset = state.componentsOffsets
var courtArea by remember { mutableStateOf(Rect.Zero) } var courtArea by state.courtArea
val playersPixelsRadius = LocalDensity.current.run { PlayerPieceDiameterDp.dp.toPx() / 2 } val playersPixelsRadius = LocalDensity.current.run { PlayerPieceDiameterDp.dp.toPx() / 2 }
Box( Box(
modifier = Modifier modifier = modifier,
.background(Color.LightGray)
.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.width(IntrinsicSize.Min)
.zoomable(zoomState), .zoomable(zoomState),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Image( Image(
painter = painterResource(id = courtImg), painter = painterResource(id = courtImg),
contentDescription = "court", contentDescription = "court",
modifier = Modifier modifier = Modifier
.background(Color.White) .background(Color.White)
.fillMaxSize()
) )
Box( Box(
@ -83,8 +88,11 @@ fun BasketCourt(content: StepContent, type: CourtType) {
drawContent() drawContent()
} }
) { ) {
for (component in components) { for (component in components) {
val modifier = Modifier val componentModifier = Modifier
.onGloballyPositioned { .onGloballyPositioned {
if (!componentsOffset.containsKey(component.id)) if (!componentsOffset.containsKey(component.id))
componentsOffset[component.id] = it.boundsInRoot().center componentsOffset[component.id] = it.boundsInRoot().center
@ -94,9 +102,8 @@ fun BasketCourt(content: StepContent, type: CourtType) {
val info = getPlayerInfo(component, content) val info = getPlayerInfo(component, content)
PlayerPiece( PlayerPiece(
player = info, player = info,
modifier = modifier modifier = componentModifier
.align(info.pos.toBiasAlignment()) .align(info.pos.toBiasAlignment())
) )
} }

@ -43,7 +43,6 @@ fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNode
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.LightGray)
.freeScroll(scrollState) .freeScroll(scrollState)
.onGloballyPositioned { .onGloballyPositioned {
if (globalOffset == Offset.Zero) if (globalOffset == Offset.Zero)

@ -5,32 +5,45 @@ import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.zIndex
import arrow.core.Either import arrow.core.Either
import com.iqball.app.R
import com.iqball.app.component.BasketCourt import com.iqball.app.component.BasketCourt
import com.iqball.app.component.BasketCourtStates
import com.iqball.app.component.StepsTree import com.iqball.app.component.StepsTree
import com.iqball.app.model.TacticInfo 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.CourtType
import com.iqball.app.model.tactic.StepNodeInfo import com.iqball.app.model.tactic.StepNodeInfo
import com.iqball.app.net.service.TacticService import com.iqball.app.net.service.TacticService
import com.iqball.app.session.Token import com.iqball.app.session.Token
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.engawapg.lib.zoomable.rememberZoomState
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
@ -47,6 +60,7 @@ fun VisualizerPage(
tacticId: Int, tacticId: Int,
) { ) {
val dataEither = remember { initializeVisualizer(service, auth, tacticId) } val dataEither = remember { initializeVisualizer(service, auth, tacticId) }
val showTree = remember { mutableStateOf(true) }
val (info, rootStep) = when (dataEither) { val (info, rootStep) = when (dataEither) {
// On error return a text to print it to the user // On error return a text to print it to the user
@ -58,13 +72,12 @@ fun VisualizerPage(
var selectedStepId by rememberSaveable { mutableIntStateOf(rootStep.id) } var selectedStepId by rememberSaveable { mutableIntStateOf(rootStep.id) }
val content = remember(selectedStepId) { val content = remember(selectedStepId) {
runBlocking { runBlocking {
val result = service.getTacticStepContent(auth, tacticId, selectedStepId) val result = service.getTacticStepContent(auth, tacticId, selectedStepId).onLeft {
.onLeft { Log.e(
Log.e( "received error response from server when retrieving root content: {}",
"received error response from server when retrieving root content: {}", it.toString()
it.toString() )
) }
}
when (result) { when (result) {
is Either.Left -> throw Error("Unexpected error") is Either.Left -> throw Error("Unexpected error")
is Either.Right -> result.value is Either.Right -> result.value
@ -72,21 +85,42 @@ fun VisualizerPage(
} }
} }
Log.d("CONTENT", content.toString())
Column { Column {
VisualizerHeader(title = info.name) VisualizerHeader(title = info.name, showTree)
when (screenOrientation) { when (screenOrientation) {
Configuration.ORIENTATION_PORTRAIT -> StepsTree( Configuration.ORIENTATION_PORTRAIT -> StepsTree(root = rootStep,
root = rootStep,
selectedNodeId = selectedStepId, selectedNodeId = selectedStepId,
onNodeSelected = { selectedStepId = it.id } onNodeSelected = { selectedStepId = it.id })
)
Configuration.ORIENTATION_LANDSCAPE -> {
val courtOffsets =
remember(showTree.value, content) { mutableStateMapOf<ComponentId, Offset>() }
val courtArea = remember(showTree.value) { mutableStateOf(Rect.Zero) }
val courtZoomState = rememberZoomState()
remember(showTree.value, content) {
runBlocking {
courtZoomState.reset()
}
}
Configuration.ORIENTATION_LANDSCAPE -> BasketCourt( val courtModifier =
content = content, if (showTree.value) Modifier.width(IntrinsicSize.Min) else Modifier.fillMaxWidth()
type = info.type
) Row(modifier = Modifier.background(Color.LightGray)) {
BasketCourt(
content = content,
type = info.type,
modifier = courtModifier,
state = BasketCourtStates(courtOffsets, courtArea, courtZoomState)
)
if (showTree.value) {
StepsTree(root = rootStep,
selectedNodeId = selectedStepId,
onNodeSelected = { selectedStepId = it.id })
}
}
}
else -> throw Exception("Could not determine device's orientation.") else -> throw Exception("Could not determine device's orientation.")
} }
@ -95,17 +129,18 @@ fun VisualizerPage(
} }
@Composable @Composable
private fun VisualizerHeader(title: String) { private fun VisualizerHeader(title: String, showTree: MutableState<Boolean>) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.zIndex(10000F)
.background(Color.White), .background(Color.White),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton( IconButton(onClick = { /*TODO*/ }) {
onClick = { /*TODO*/ }
) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
@ -115,44 +150,39 @@ private fun VisualizerHeader(title: String) {
Text(text = title, color = Color.Black) Text(text = title, color = Color.Black)
Text(text = "") IconButton(onClick = { showTree.value = !showTree.value }) {
Icon(
painter = painterResource(id = R.drawable.tree_icon),
contentDescription = "toggle show tree"
)
}
} }
} }
private fun initializeVisualizer( private fun initializeVisualizer(
service: TacticService, service: TacticService, auth: Token, tacticId: Int
auth: Token,
tacticId: Int
): Either<String, VisualizerInitialData> { ): Either<String, VisualizerInitialData> {
val (tacticInfo, tacticTree) = runBlocking { val (tacticInfo, tacticTree) = runBlocking {
val tacticInfo = service.getTacticInfo(auth, tacticId) val tacticInfo = service.getTacticInfo(auth, tacticId).map {
.map { TacticInfo(
TacticInfo( id = it.id, name = it.name, type = CourtType.valueOf(
id = it.id, it.courtType.lowercase().replaceFirstChar(Char::uppercaseChar)
name = it.name, ), creationDate = LocalDateTime.ofInstant(
type = CourtType.valueOf( Instant.ofEpochMilli(it.creationDate), ZoneId.systemDefault()
it.courtType.lowercase().replaceFirstChar(Char::uppercaseChar)
),
creationDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(it.creationDate),
ZoneId.systemDefault()
)
) )
}.onLeft { )
Log.e( }.onLeft {
"received error response from server when retrieving tacticInfo : {}", Log.e(
it.toString() "received error response from server when retrieving tacticInfo : {}", it.toString()
) )
} }
val tacticTree = service.getTacticStepsTree(auth, tacticId) val tacticTree = service.getTacticStepsTree(auth, tacticId).map { it.root }.onLeft {
.map { it.root } Log.e(
.onLeft { "received error response from server when retrieving tactic steps tree: {}",
Log.e( it.toString()
"received error response from server when retrieving tactic steps tree: {}", )
it.toString() }
)
}
Pair(tacticInfo.getOrNull(), tacticTree.getOrNull()) Pair(tacticInfo.getOrNull(), tacticTree.getOrNull())
} }

@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.3.0" agp = "8.2.2"
arrowCore = "1.2.1" arrowCore = "1.2.1"
composeFreeScroll = "0.2.2" composeFreeScroll = "0.2.2"
converterGson = "2.9.0" converterGson = "2.9.0"

Loading…
Cancel
Save