From 53d3254f7b740e7a1cb42dab57d35d81b5f74b4e Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Thu, 21 Mar 2024 11:19:42 +0100 Subject: [PATCH] Steps tree is now displayed alongside the visualizer --- .../main/java/com/iqball/app/MainActivity.kt | 4 +- .../com/iqball/app/component/BasketCourt.kt | 41 +++--- .../java/com/iqball/app/component/StepTree.kt | 1 - .../com/iqball/app/page/VisualizerPage.kt | 132 +++++++++++------- gradle/libs.versions.toml | 2 +- 5 files changed, 108 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/iqball/app/MainActivity.kt b/app/src/main/java/com/iqball/app/MainActivity.kt index 692f77f..9093d0d 100644 --- a/app/src/main/java/com/iqball/app/MainActivity.kt +++ b/app/src/main/java/com/iqball/app/MainActivity.kt @@ -67,7 +67,7 @@ class MainActivity : ComponentActivity() { .addConverterFactory(EitherBodyConverter.create()) .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(EitherCallAdapterFactory.create()) - .baseUrl("http://grospc:5254/") + .baseUrl("http://192.168.127.83:5254/") .client( OkHttpClient.Builder() .addInterceptor { it.proceed(it.request()) } @@ -98,5 +98,5 @@ fun App(service: IQBallService) { auth.getOrNull()!!.token } - VisualizerPage(service, auth, 21) + VisualizerPage(service, auth, 3) } \ No newline at end of file diff --git a/app/src/main/java/com/iqball/app/component/BasketCourt.kt b/app/src/main/java/com/iqball/app/component/BasketCourt.kt index cb17aaf..8b5d87c 100644 --- a/app/src/main/java/com/iqball/app/component/BasketCourt.kt +++ b/app/src/main/java/com/iqball/app/component/BasketCourt.kt @@ -3,14 +3,10 @@ package com.iqball.app.component import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState 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.ui.Alignment 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.PlayerLike import com.iqball.app.model.tactic.StepContent +import net.engawapg.lib.zoomable.ZoomState import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable +data class BasketCourtStates( + val componentsOffsets: MutableMap, + val courtArea: MutableState, + val zoomState: ZoomState +) + @Composable -fun BasketCourt(content: StepContent, type: CourtType) { +fun BasketCourt( + content: StepContent, + type: CourtType, + modifier: Modifier, + state: BasketCourtStates +) { val courtImg = when (type) { CourtType.Plain -> R.drawable.plain_court CourtType.Half -> R.drawable.half_court } - val zoomState = rememberZoomState() + val zoomState = state.zoomState val components = content.components - val componentsOffset = remember { mutableStateMapOf() } - var courtArea by remember { mutableStateOf(Rect.Zero) } + val componentsOffset = state.componentsOffsets + var courtArea by state.courtArea val playersPixelsRadius = LocalDensity.current.run { PlayerPieceDiameterDp.dp.toPx() / 2 } Box( - modifier = Modifier - .background(Color.LightGray) - .fillMaxSize(), + modifier = modifier, contentAlignment = Alignment.Center ) { Box( modifier = Modifier - .width(IntrinsicSize.Min) .zoomable(zoomState), contentAlignment = Alignment.Center, ) { + Image( painter = painterResource(id = courtImg), contentDescription = "court", modifier = Modifier .background(Color.White) - .fillMaxSize() ) Box( @@ -83,8 +88,11 @@ fun BasketCourt(content: StepContent, type: CourtType) { drawContent() } ) { + + + for (component in components) { - val modifier = Modifier + val componentModifier = Modifier .onGloballyPositioned { if (!componentsOffset.containsKey(component.id)) componentsOffset[component.id] = it.boundsInRoot().center @@ -94,9 +102,8 @@ fun BasketCourt(content: StepContent, type: CourtType) { val info = getPlayerInfo(component, content) PlayerPiece( player = info, - modifier = modifier + modifier = componentModifier .align(info.pos.toBiasAlignment()) - ) } diff --git a/app/src/main/java/com/iqball/app/component/StepTree.kt b/app/src/main/java/com/iqball/app/component/StepTree.kt index 21a55af..c8fec61 100644 --- a/app/src/main/java/com/iqball/app/component/StepTree.kt +++ b/app/src/main/java/com/iqball/app/component/StepTree.kt @@ -43,7 +43,6 @@ fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNode Box( modifier = Modifier .fillMaxSize() - .background(Color.LightGray) .freeScroll(scrollState) .onGloballyPositioned { if (globalOffset == Offset.Zero) diff --git a/app/src/main/java/com/iqball/app/page/VisualizerPage.kt b/app/src/main/java/com/iqball/app/page/VisualizerPage.kt index 882b1a5..6fe5499 100644 --- a/app/src/main/java/com/iqball/app/page/VisualizerPage.kt +++ b/app/src/main/java/com/iqball/app/page/VisualizerPage.kt @@ -5,32 +5,45 @@ 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.StepNodeInfo import com.iqball.app.net.service.TacticService import com.iqball.app.session.Token import kotlinx.coroutines.runBlocking +import net.engawapg.lib.zoomable.rememberZoomState import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId @@ -47,6 +60,7 @@ fun VisualizerPage( tacticId: Int, ) { val dataEither = remember { initializeVisualizer(service, auth, tacticId) } + val showTree = remember { mutableStateOf(true) } val (info, rootStep) = when (dataEither) { // On error return a text to print it to the user @@ -58,13 +72,12 @@ fun VisualizerPage( var selectedStepId by rememberSaveable { mutableIntStateOf(rootStep.id) } val content = remember(selectedStepId) { runBlocking { - val result = service.getTacticStepContent(auth, tacticId, selectedStepId) - .onLeft { - Log.e( - "received error response from server when retrieving root content: {}", - it.toString() - ) - } + val result = service.getTacticStepContent(auth, tacticId, selectedStepId).onLeft { + Log.e( + "received error response from server when retrieving root content: {}", + it.toString() + ) + } when (result) { is Either.Left -> throw Error("Unexpected error") is Either.Right -> result.value @@ -72,21 +85,42 @@ fun VisualizerPage( } } - Log.d("CONTENT", content.toString()) Column { - VisualizerHeader(title = info.name) + VisualizerHeader(title = info.name, showTree) when (screenOrientation) { - Configuration.ORIENTATION_PORTRAIT -> StepsTree( - root = rootStep, + Configuration.ORIENTATION_PORTRAIT -> StepsTree(root = rootStep, selectedNodeId = selectedStepId, - onNodeSelected = { selectedStepId = it.id } - ) + onNodeSelected = { selectedStepId = it.id }) + + Configuration.ORIENTATION_LANDSCAPE -> { + val courtOffsets = + remember(showTree.value, content) { mutableStateMapOf() } + val courtArea = remember(showTree.value) { mutableStateOf(Rect.Zero) } + val courtZoomState = rememberZoomState() + remember(showTree.value, content) { + runBlocking { + courtZoomState.reset() + } + } - Configuration.ORIENTATION_LANDSCAPE -> BasketCourt( - content = content, - type = info.type - ) + val courtModifier = + if (showTree.value) Modifier.width(IntrinsicSize.Min) else Modifier.fillMaxWidth() + + 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.") } @@ -95,17 +129,18 @@ fun VisualizerPage( } @Composable -private fun VisualizerHeader(title: String) { +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*/ } - ) { + IconButton(onClick = { /*TODO*/ }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", @@ -115,44 +150,39 @@ private fun VisualizerHeader(title: String) { 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( - service: TacticService, - auth: Token, - tacticId: Int + 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() - ) + 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() - ) - } + ) + }.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 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()) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7ebd02..c1dcf90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.3.0" +agp = "8.2.2" arrowCore = "1.2.1" composeFreeScroll = "0.2.2" converterGson = "2.9.0"