commit
47311389bd
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 6.0 KiB |
@ -0,0 +1,330 @@
|
||||
package com.iqball.app.component
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import arrow.core.Either
|
||||
import com.iqball.app.geo.Vector
|
||||
import com.iqball.app.model.tactic.Action
|
||||
import com.iqball.app.model.tactic.ActionType
|
||||
import com.iqball.app.model.tactic.ComponentId
|
||||
import com.iqball.app.model.tactic.Segment
|
||||
import com.iqball.app.model.tactic.StepContent
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
|
||||
private const val ArrowWidthPx = 5F
|
||||
private const val ArrowHeadHeightPx = 35F
|
||||
|
||||
fun drawActions(
|
||||
drawer: ContentDrawScope,
|
||||
content: StepContent,
|
||||
offsets: Map<ComponentId, Vector>,
|
||||
area: Rect,
|
||||
playersPixelsRadius: Float,
|
||||
color: Color
|
||||
) {
|
||||
for (component in content.components) {
|
||||
val originPos = offsets[component.id]!!
|
||||
for (action in component.actions) {
|
||||
|
||||
val type = action.type
|
||||
|
||||
val forceStraight = type == ActionType.Shoot
|
||||
|
||||
val strokeStyle = when (type) {
|
||||
ActionType.Screen, ActionType.Move, ActionType.Dribble -> Stroke(width = ArrowWidthPx)
|
||||
ActionType.Shoot -> Stroke(
|
||||
width = ArrowWidthPx,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(15F, 15F))
|
||||
)
|
||||
}
|
||||
|
||||
// draw the arrow body
|
||||
if (forceStraight) {
|
||||
val targetOffset = when (val value = action.target) {
|
||||
is Either.Left -> offsets[value.value]!!
|
||||
is Either.Right -> value.value.posWithinArea(area)
|
||||
}
|
||||
val path = Path()
|
||||
val pathStart = constraintInCircle(
|
||||
originPos,
|
||||
getOrientationPointFromSegmentBounds(
|
||||
action.segments,
|
||||
originPos,
|
||||
offsets,
|
||||
area,
|
||||
false
|
||||
),
|
||||
playersPixelsRadius
|
||||
)
|
||||
val pathEnd = constraintInCircle(
|
||||
targetOffset,
|
||||
getOrientationPointFromSegmentBounds(
|
||||
action.segments,
|
||||
originPos,
|
||||
offsets,
|
||||
area,
|
||||
true
|
||||
),
|
||||
playersPixelsRadius + ArrowHeadHeightPx
|
||||
)
|
||||
path.moveTo(pathStart.x, pathStart.y)
|
||||
path.lineTo(pathEnd.x, pathEnd.y)
|
||||
drawer.drawPath(
|
||||
path = path,
|
||||
color = color,
|
||||
style = strokeStyle
|
||||
)
|
||||
} else {
|
||||
drawer.drawPath(
|
||||
path = computeSegmentsToPath(
|
||||
originPos,
|
||||
action.segments,
|
||||
offsets,
|
||||
area,
|
||||
type == ActionType.Dribble,
|
||||
playersPixelsRadius
|
||||
),
|
||||
color = color,
|
||||
style = strokeStyle
|
||||
)
|
||||
}
|
||||
|
||||
drawArrowHead(drawer, originPos, action, offsets, area, playersPixelsRadius)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawArrowHead(
|
||||
drawer: DrawScope,
|
||||
originPos: Vector,
|
||||
action: Action,
|
||||
offsets: Map<ComponentId, Vector>,
|
||||
area: Rect,
|
||||
playersPixelsRadius: Float
|
||||
) {
|
||||
val segments = action.segments
|
||||
val lastSegment = segments.last()
|
||||
val target = lastSegment.next
|
||||
var targetPos = extractPos(target, offsets, area)
|
||||
val segmentOrientationPoint =
|
||||
getOrientationPointFromSegmentBounds(segments, originPos, offsets, area, true)
|
||||
|
||||
targetPos = constraintInCircle(targetPos, segmentOrientationPoint, playersPixelsRadius)
|
||||
|
||||
val pathAngleToTarget = -targetPos.angleDegWith(segmentOrientationPoint)
|
||||
|
||||
drawer.rotate(pathAngleToTarget, pivot = targetPos.toOffset()) {
|
||||
|
||||
val path =
|
||||
if (action.type == ActionType.Screen) getRectangleHeadPath(targetPos) else getTriangleHeadPath(
|
||||
targetPos
|
||||
)
|
||||
drawPath(path = path, color = Color.Black, style = Fill)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTriangleHeadPath(start: Vector): Path {
|
||||
val path = Path()
|
||||
path.moveTo(start.x, start.y)
|
||||
path.relativeLineTo(-ArrowHeadHeightPx, -ArrowHeadHeightPx)
|
||||
path.relativeLineTo(ArrowHeadHeightPx * 2, 0F)
|
||||
return path
|
||||
}
|
||||
|
||||
private fun getRectangleHeadPath(start: Vector): Path {
|
||||
val path = Path()
|
||||
path.moveTo(start.x, start.y - ArrowHeadHeightPx / 2F)
|
||||
path.relativeLineTo(-ArrowHeadHeightPx, 0F)
|
||||
path.relativeLineTo(0F, -ArrowHeadHeightPx / 2F)
|
||||
path.relativeLineTo(ArrowHeadHeightPx * 2, 0F)
|
||||
path.relativeLineTo(0F, ArrowHeadHeightPx / 2F)
|
||||
return path
|
||||
}
|
||||
|
||||
private fun getOrientationPointFromSegmentBounds(
|
||||
segments: List<Segment>,
|
||||
originPos: Vector,
|
||||
offsets: Map<ComponentId, Vector>,
|
||||
area: Rect,
|
||||
head: Boolean
|
||||
): Vector {
|
||||
val boundSegment = if (head) segments.last() else segments.first()
|
||||
return boundSegment
|
||||
.controlPoint?.posWithinArea(area)
|
||||
?: if (segments.size == 1)
|
||||
if (head) originPos else extractPos(segments.last().next, offsets, area)
|
||||
else run {
|
||||
val referenceSegment = if (head) segments[segments.size - 2] else segments[1]
|
||||
extractPos(referenceSegment.next, offsets, area)
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeSegmentsToPath(
|
||||
originPos: Vector,
|
||||
segments: List<Segment>,
|
||||
offsets: Map<ComponentId, Vector>,
|
||||
area: Rect,
|
||||
wavy: Boolean,
|
||||
playersPixelsRadius: Float
|
||||
): Path {
|
||||
val path = Path()
|
||||
|
||||
|
||||
val firstSegment = segments.first()
|
||||
|
||||
var segmentStart = constraintInCircle(
|
||||
originPos,
|
||||
getOrientationPointFromSegmentBounds(segments, originPos, offsets, area, false),
|
||||
playersPixelsRadius
|
||||
)
|
||||
|
||||
var lastSegmentCp =
|
||||
firstSegment.controlPoint?.posWithinArea(area) ?: (segmentStart + extractPos(
|
||||
firstSegment.next, offsets, area
|
||||
) / 2F)
|
||||
|
||||
path.moveTo(segmentStart.x, segmentStart.y)
|
||||
|
||||
|
||||
for ((i, segment) in segments.withIndex()) {
|
||||
var nextPos = extractPos(segment.next, offsets, area)
|
||||
|
||||
if (i == segments.size - 1) {
|
||||
// if it is the last segment, the next position must be constrained to the player's radius
|
||||
nextPos = constraintInCircle(
|
||||
nextPos,
|
||||
getOrientationPointFromSegmentBounds(
|
||||
segments,
|
||||
originPos,
|
||||
offsets,
|
||||
area,
|
||||
true
|
||||
),
|
||||
playersPixelsRadius + ArrowHeadHeightPx
|
||||
)
|
||||
}
|
||||
|
||||
val segmentCp = segment.controlPoint?.posWithinArea(area) ?: ((segmentStart + nextPos) / 2F)
|
||||
val castedCp =
|
||||
if (i == 0) segmentCp else segmentStart + (segmentStart - lastSegmentCp)
|
||||
|
||||
if (wavy) {
|
||||
wavyBezier(segmentStart, castedCp, segmentCp, nextPos, 7, 15, path)
|
||||
} else {
|
||||
path.cubicTo(castedCp.x, castedCp.y, segmentCp.x, segmentCp.y, nextPos.x, nextPos.y)
|
||||
}
|
||||
|
||||
lastSegmentCp = segmentCp
|
||||
segmentStart = nextPos
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
private fun cubicBeziersDerivative(
|
||||
start: Vector,
|
||||
cp1: Vector,
|
||||
cp2: Vector,
|
||||
end: Vector,
|
||||
t: Float
|
||||
): Vector = ((cp1 - start) * 3F * (1 - t).pow(2)) +
|
||||
((cp2 - cp1) * 6F * (1 - t) * t) +
|
||||
((end - cp2) * 3F * t.pow(2))
|
||||
|
||||
|
||||
private fun cubicBeziers(
|
||||
start: Vector,
|
||||
cp1: Vector,
|
||||
cp2: Vector,
|
||||
end: Vector,
|
||||
t: Float
|
||||
): Vector = (start * (1 - t).pow(3)) +
|
||||
(cp1 * 3F * t * (1 - t).pow(2)) +
|
||||
(cp2 * 3F * t.pow(2) * (1 - t)) +
|
||||
(end * t.pow(3))
|
||||
|
||||
private fun wavyBezier(
|
||||
start: Vector,
|
||||
cp1: Vector,
|
||||
cp2: Vector,
|
||||
end: Vector,
|
||||
wavesPer100Px: Int,
|
||||
amplitude: Int,
|
||||
path: Path
|
||||
) {
|
||||
|
||||
fun getVerticalDerivativeProjectionAmplification(t: Float): Vector {
|
||||
val velocity = cubicBeziersDerivative(start, cp1, cp2, end, t)
|
||||
val velocityLength = velocity.norm()
|
||||
val projection = Vector(velocity.y, -velocity.x)
|
||||
|
||||
return (projection / velocityLength) * amplitude
|
||||
}
|
||||
|
||||
|
||||
val dist = start.distanceFrom(cp1) + cp1.distanceFrom(cp2) + cp2.distanceFrom(end)
|
||||
|
||||
val waveLength = (dist / 100) * wavesPer100Px * 2
|
||||
val step = 1F / waveLength
|
||||
|
||||
// 0 : middle to up
|
||||
// 1 : up to middle
|
||||
// 2 : middle to down
|
||||
// 3 : down to middle
|
||||
var phase = 0
|
||||
|
||||
var t = step
|
||||
while (t <= 1) {
|
||||
val pos = cubicBeziers(start, cp1, cp2, end, t)
|
||||
val amplification = getVerticalDerivativeProjectionAmplification(t)
|
||||
|
||||
val nextPos = when (phase) {
|
||||
1, 3 -> pos
|
||||
0 -> pos + amplification
|
||||
else -> pos - amplification
|
||||
}
|
||||
|
||||
val controlPointBase = cubicBeziers(start, cp1, cp2, end, t - step / 2)
|
||||
val controlPoint =
|
||||
if (phase == 0 || phase == 1) controlPointBase + amplification else controlPointBase - amplification
|
||||
path.quadraticBezierTo(controlPoint.x, controlPoint.y, nextPos.x, nextPos.y)
|
||||
phase = (phase + 1) % 4
|
||||
t += step
|
||||
if (t < 1 && t > 1 - step) t = 1F
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a circle shaped by a central position, and a radius, return
|
||||
* a position that is constrained on its perimeter, pointing to the direction
|
||||
* between the circle's center and the reference position.
|
||||
* @param center circle's center.
|
||||
* @param reference a reference point used to create the angle where the returned position
|
||||
* will point to on the circle's perimeter
|
||||
* @param radius circle's radius.
|
||||
*/
|
||||
private fun constraintInCircle(center: Vector, reference: Vector, radius: Float): Vector {
|
||||
val theta = center.angleRadWith(reference)
|
||||
return Vector(
|
||||
x = center.x - sin(theta) * radius,
|
||||
y = center.y - cos(theta) * radius
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractPos(
|
||||
next: Either<ComponentId, Vector>,
|
||||
offsets: Map<ComponentId, Vector>,
|
||||
area: Rect
|
||||
) = when (next) {
|
||||
is Either.Left -> offsets[next.value]!!
|
||||
is Either.Right -> next.value.posWithinArea(area)
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.iqball.app.component
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.iqball.app.R
|
||||
import com.iqball.app.ui.theme.BallColor
|
||||
|
||||
const val BallPieceDiameterDp = 20
|
||||
|
||||
@Composable
|
||||
fun BallPiece(modifier: Modifier = Modifier) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ball),
|
||||
contentDescription = "ball",
|
||||
tint = BallColor,
|
||||
modifier = modifier.size(BallPieceDiameterDp.dp)
|
||||
)
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
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.requiredHeight
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.boundsInParent
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import arrow.core.getOrNone
|
||||
import com.iqball.app.R
|
||||
import com.iqball.app.domains.getPlayerInfo
|
||||
import com.iqball.app.geo.toVector
|
||||
import com.iqball.app.model.tactic.BallComponent
|
||||
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.zoomable
|
||||
|
||||
data class BasketCourtStates(
|
||||
val stepComponentsOffsets: MutableMap<ComponentId, Offset>,
|
||||
val parentComponentsOffsets: MutableMap<ComponentId, Offset>,
|
||||
val courtArea: MutableState<Rect>,
|
||||
val zoomState: ZoomState
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BasketCourt(
|
||||
content: StepContent,
|
||||
parentContent: StepContent?,
|
||||
type: CourtType,
|
||||
modifier: Modifier,
|
||||
state: BasketCourtStates
|
||||
) {
|
||||
val courtImg = when (type) {
|
||||
CourtType.Plain -> R.drawable.plain_court
|
||||
CourtType.Half -> R.drawable.half_court
|
||||
}
|
||||
|
||||
var courtArea by state.courtArea
|
||||
val zoomState = state.zoomState
|
||||
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(Color.LightGray)
|
||||
.fillMaxSize(),
|
||||
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)
|
||||
.onGloballyPositioned {
|
||||
if (courtArea == Rect.Zero)
|
||||
courtArea = it.boundsInRoot()
|
||||
}
|
||||
)
|
||||
|
||||
CourtContent(
|
||||
courtArea = courtArea,
|
||||
content = content,
|
||||
offsets = state.stepComponentsOffsets,
|
||||
isFromParent = false
|
||||
)
|
||||
if (parentContent != null) {
|
||||
CourtContent(
|
||||
courtArea = courtArea,
|
||||
content = parentContent,
|
||||
offsets = state.parentComponentsOffsets,
|
||||
isFromParent = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CourtContent(
|
||||
courtArea: Rect,
|
||||
content: StepContent,
|
||||
offsets: MutableMap<ComponentId, Offset>,
|
||||
isFromParent: Boolean
|
||||
) {
|
||||
val playersPixelsRadius = LocalDensity.current.run { PlayerPieceDiameterDp.dp.toPx() / 2 }
|
||||
|
||||
val width = LocalDensity.current.run { courtArea.width.toDp() }
|
||||
val height = LocalDensity.current.run { courtArea.height.toDp() }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(width)
|
||||
.requiredHeight(height)
|
||||
.drawWithContent {
|
||||
val relativeOffsets =
|
||||
offsets.mapValues { (it.value).toVector() }
|
||||
drawActions(
|
||||
this,
|
||||
content,
|
||||
relativeOffsets,
|
||||
courtArea,
|
||||
playersPixelsRadius,
|
||||
if (isFromParent) Color.Gray else Color.Black
|
||||
)
|
||||
drawContent()
|
||||
}
|
||||
) {
|
||||
|
||||
|
||||
for (component in content.components) {
|
||||
val componentModifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
if (!offsets.getOrNone(component.id).isSome { it != Offset.Zero })
|
||||
offsets[component.id] = it.boundsInParent().center
|
||||
}
|
||||
when (component) {
|
||||
is PlayerLike -> {
|
||||
val info = getPlayerInfo(component, content)
|
||||
PlayerPiece(
|
||||
player = info,
|
||||
isFromParent = isFromParent,
|
||||
modifier = componentModifier
|
||||
.align(info.pos.toBiasAlignment())
|
||||
)
|
||||
}
|
||||
|
||||
is BallComponent -> BallPiece(
|
||||
modifier = componentModifier
|
||||
.align(
|
||||
component.pos
|
||||
.toPos()
|
||||
.toBiasAlignment()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.iqball.app.component
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.iqball.app.model.tactic.PlayerInfo
|
||||
import com.iqball.app.model.tactic.PlayerTeam
|
||||
import com.iqball.app.ui.theme.Allies
|
||||
import com.iqball.app.ui.theme.Opponents
|
||||
|
||||
const val PlayerPieceDiameterDp = 25
|
||||
|
||||
@Composable
|
||||
fun PlayerPiece(player: PlayerInfo, modifier: Modifier = Modifier, isFromParent: Boolean) {
|
||||
|
||||
val color = if (isFromParent) Color.LightGray else if (player.team === PlayerTeam.Allies) Allies else Opponents
|
||||
|
||||
return Surface(
|
||||
shape = CircleShape,
|
||||
border = if (player.ballState.hasBall()) BorderStroke(2.dp, Color.Black) else null,
|
||||
modifier = modifier
|
||||
.alpha(if (player.isPhantom) .5F else 1F)
|
||||
) {
|
||||
Text(
|
||||
text = player.role,
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.Black,
|
||||
modifier = Modifier
|
||||
.background(color)
|
||||
.size(PlayerPieceDiameterDp.dp)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package com.iqball.app.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.chihsuanwu.freescroll.freeScroll
|
||||
import com.chihsuanwu.freescroll.rememberFreeScrollState
|
||||
import com.iqball.app.domains.getStepName
|
||||
import com.iqball.app.model.tactic.StepNodeInfo
|
||||
import com.iqball.app.ui.theme.SelectedStepNode
|
||||
import com.iqball.app.ui.theme.StepNode
|
||||
|
||||
@Composable
|
||||
fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNodeInfo) -> Unit) {
|
||||
|
||||
val scrollState = rememberFreeScrollState()
|
||||
val nodesOffsets = remember { mutableStateMapOf<StepNodeInfo, Rect>() }
|
||||
var globalOffset by remember { mutableStateOf(Offset.Zero) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.freeScroll(scrollState)
|
||||
.onGloballyPositioned {
|
||||
if (globalOffset == Offset.Zero)
|
||||
globalOffset = it.boundsInRoot().topLeft
|
||||
}
|
||||
.drawWithContent {
|
||||
|
||||
if (nodesOffsets.isEmpty()) {
|
||||
drawContent()
|
||||
return@drawWithContent
|
||||
}
|
||||
|
||||
val toDraw = mutableListOf(root)
|
||||
while (toDraw.isNotEmpty()) {
|
||||
val parent = toDraw.removeLast()
|
||||
val parentCenter = nodesOffsets[parent]!!.center - globalOffset
|
||||
for (children in parent.children) {
|
||||
val childrenCenter = nodesOffsets[children]!!.center - globalOffset
|
||||
drawLine(
|
||||
Color.Black,
|
||||
start = parentCenter,
|
||||
end = childrenCenter,
|
||||
strokeWidth = 5F
|
||||
)
|
||||
toDraw += children
|
||||
}
|
||||
}
|
||||
|
||||
drawContent()
|
||||
|
||||
},
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
StepsTreeContent(root, root, selectedNodeId, onNodeSelected, nodesOffsets)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StepsTreeContent(
|
||||
root: StepNodeInfo,
|
||||
node: StepNodeInfo,
|
||||
selectedNodeId: Int,
|
||||
onNodeSelected: (StepNodeInfo) -> Unit,
|
||||
nodesOffsets: MutableMap<StepNodeInfo, Rect>
|
||||
) {
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
) {
|
||||
StepPiece(
|
||||
name = getStepName(root, node.id),
|
||||
node = node,
|
||||
isSelected = selectedNodeId == node.id,
|
||||
onNodeSelected = { onNodeSelected(node) },
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.onGloballyPositioned {
|
||||
if (!nodesOffsets.containsKey(node))
|
||||
nodesOffsets[node] = it.boundsInRoot()
|
||||
}
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 50.dp)
|
||||
) {
|
||||
for (children in node.children) {
|
||||
StepsTreeContent(
|
||||
root = root,
|
||||
node = children,
|
||||
selectedNodeId = selectedNodeId,
|
||||
onNodeSelected = onNodeSelected,
|
||||
nodesOffsets = nodesOffsets
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StepPiece(
|
||||
name: String,
|
||||
node: StepNodeInfo,
|
||||
isSelected: Boolean,
|
||||
onNodeSelected: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val color = if (isSelected) SelectedStepNode else StepNode
|
||||
|
||||
return Surface(
|
||||
shape = CircleShape,
|
||||
modifier = modifier.clickable {
|
||||
onNodeSelected()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
textAlign = TextAlign.Center,
|
||||
color = if (isSelected) Color.White else Color.Black,
|
||||
modifier = Modifier
|
||||
.background(color)
|
||||
.size(PlayerPieceDiameterDp.dp)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package com.iqball.app.domains
|
||||
|
||||
import arrow.core.merge
|
||||
import com.iqball.app.geo.Vector
|
||||
import com.iqball.app.model.tactic.BallComponent
|
||||
import com.iqball.app.model.tactic.FixedPosition
|
||||
import com.iqball.app.model.tactic.MalformedStepContentException
|
||||
import com.iqball.app.model.tactic.PhantomComponent
|
||||
import com.iqball.app.model.tactic.PlayerComponent
|
||||
import com.iqball.app.model.tactic.PlayerInfo
|
||||
import com.iqball.app.model.tactic.PlayerLike
|
||||
import com.iqball.app.model.tactic.RelativePositioning
|
||||
import com.iqball.app.model.tactic.StepComponent
|
||||
import com.iqball.app.model.tactic.StepContent
|
||||
|
||||
/**
|
||||
* Converts the phantom's [Positioning] to a XY Position
|
||||
* if the phantom is a [RelativePositioning], the XY coords are determined
|
||||
* using the attached component, and by expecting that there is an action on the attached component that
|
||||
* targets the given phantom.
|
||||
* If so, then the position is determined by projecting the attached component's position, and the direction
|
||||
* of the action's last segment.
|
||||
* @throws MalformedStepContentException if the step content contains incoherent data
|
||||
*/
|
||||
fun computePhantomPosition(phantom: PhantomComponent, content: StepContent): Vector {
|
||||
return when (val pos = phantom.pos) {
|
||||
is FixedPosition -> pos.toPos()
|
||||
|
||||
is RelativePositioning -> {
|
||||
val phantomBefore = getPlayerBefore(phantom, content)!!
|
||||
|
||||
val referentId = pos.attach
|
||||
val actions = phantomBefore.actions
|
||||
val linkAction = actions.find { it.target.isLeft(referentId::equals) }
|
||||
?: throw MalformedStepContentException("phantom ${phantom.id} is casted by ${phantom}, but there is no action between them.")
|
||||
|
||||
val segments = linkAction.segments
|
||||
val lastSegment = segments.last()
|
||||
|
||||
val referent = content.findComponent<StepComponent>(referentId)!!
|
||||
val referentPos = computeComponentPosition(referent, content)
|
||||
val directionalPos = lastSegment.controlPoint
|
||||
?: segments.elementAtOrNull(segments.size - 2)
|
||||
?.next
|
||||
?.mapLeft { computeComponentPosition(content.findComponent(it)!!, content) }
|
||||
?.merge()
|
||||
?: computeComponentPosition(phantomBefore, content)
|
||||
|
||||
val axisSegment = (directionalPos - referentPos)
|
||||
val segmentLength = axisSegment.norm()
|
||||
val projectedVector = Vector(
|
||||
x = (axisSegment.x / segmentLength) * 0.05F,
|
||||
y = (axisSegment.y / segmentLength) * 0.05F,
|
||||
)
|
||||
|
||||
referentPos + projectedVector
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun computeComponentPosition(component: StepComponent, content: StepContent): Vector =
|
||||
when (component) {
|
||||
is PhantomComponent -> computePhantomPosition(component, content)
|
||||
is PlayerComponent -> component.pos.toPos()
|
||||
is BallComponent -> component.pos.toPos()
|
||||
}
|
||||
|
||||
|
||||
fun getPlayerBefore(phantom: PhantomComponent, content: StepContent): PlayerLike? {
|
||||
val origin = content.findComponent<PlayerComponent>(phantom.originPlayerId)!!
|
||||
val items = origin.path!!.items
|
||||
val phantomIdx = items.indexOf(phantom.id)
|
||||
if (phantomIdx == -1)
|
||||
throw MalformedStepContentException("phantom player is not registered it its origin's path")
|
||||
if (phantomIdx == 0)
|
||||
return origin
|
||||
return content.findComponent<PhantomComponent>(items[phantomIdx - 1])
|
||||
}
|
||||
|
||||
fun getPlayerInfo(player: PlayerLike, content: StepContent): PlayerInfo {
|
||||
|
||||
return when (player) {
|
||||
is PlayerComponent -> PlayerInfo(
|
||||
player.team,
|
||||
player.role,
|
||||
false,
|
||||
player.pos.toPos(),
|
||||
player.id,
|
||||
player.actions,
|
||||
player.ballState
|
||||
)
|
||||
|
||||
is PhantomComponent -> {
|
||||
val origin = content.findComponent<PlayerComponent>(player.originPlayerId)!!
|
||||
val pos = computePhantomPosition(player, content)
|
||||
|
||||
PlayerInfo(
|
||||
origin.team,
|
||||
origin.role,
|
||||
true,
|
||||
pos,
|
||||
player.id,
|
||||
player.actions,
|
||||
player.ballState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.iqball.app.domains
|
||||
|
||||
import com.iqball.app.model.tactic.StepNodeInfo
|
||||
|
||||
|
||||
fun getStepName(root: StepNodeInfo, step: Int): String {
|
||||
var ord = 1
|
||||
val nodes = mutableListOf(root)
|
||||
while (nodes.isNotEmpty()) {
|
||||
val node = nodes.removeFirst()
|
||||
|
||||
if (node.id == step) break
|
||||
|
||||
ord += 1
|
||||
nodes.addAll(node.children.reversed())
|
||||
}
|
||||
|
||||
return ord.toString()
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.iqball.app.geo
|
||||
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.sqrt
|
||||
|
||||
typealias Pos = Vector
|
||||
|
||||
data class Vector(val x: Float, val y: Float) {
|
||||
fun toBiasAlignment(): BiasAlignment =
|
||||
BiasAlignment((x * 2 - 1), (y * 2 - 1))
|
||||
|
||||
infix operator fun minus(other: Vector) = Vector(x - other.x, y - other.y)
|
||||
infix operator fun plus(other: Vector) = Vector(x + other.x, y + other.y)
|
||||
infix operator fun div(other: Vector) = Vector(x / other.x, y / other.y)
|
||||
infix operator fun div(n: Float) = Vector(x / n, y / n)
|
||||
|
||||
infix operator fun times(other: Vector) = Vector(x * other.x, y * other.y)
|
||||
infix operator fun times(n: Float) = Vector(x * n, y * n)
|
||||
infix operator fun times(n: Int) = Vector(x * n, y * n)
|
||||
|
||||
|
||||
fun angleRadWith(other: Vector): Float {
|
||||
val (x, y) = this - other
|
||||
return atan2(x, y)
|
||||
}
|
||||
|
||||
fun angleDegWith(other: Vector): Float = (angleRadWith(other) * (180 / Math.PI)).toFloat()
|
||||
|
||||
|
||||
fun distanceFrom(other: Vector) =
|
||||
sqrt(((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y)))
|
||||
|
||||
fun norm() = NullVector.distanceFrom(this)
|
||||
|
||||
fun posWithinArea(area: Rect) = Vector(x * area.width, y * area.height)
|
||||
|
||||
fun toOffset() = Offset(x, y)
|
||||
}
|
||||
|
||||
val NullVector = Vector(0F, 0F)
|
||||
|
||||
fun Offset.toVector() = Vector(x, y)
|
@ -0,0 +1,11 @@
|
||||
package com.iqball.app.model
|
||||
|
||||
import com.iqball.app.model.tactic.CourtType
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class TacticInfo(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val type: CourtType,
|
||||
val creationDate: LocalDateTime
|
||||
)
|
@ -0,0 +1,24 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.NonEmptyList
|
||||
import com.iqball.app.geo.Vector
|
||||
|
||||
|
||||
enum class ActionType {
|
||||
Screen,
|
||||
Dribble,
|
||||
Move,
|
||||
Shoot
|
||||
}
|
||||
|
||||
data class Segment(
|
||||
val next: Either<ComponentId, Vector>,
|
||||
val controlPoint: Vector?
|
||||
)
|
||||
|
||||
data class Action(
|
||||
val type: ActionType,
|
||||
val target: Either<ComponentId, Vector>,
|
||||
val segments: List<Segment>
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
enum class BallState {
|
||||
None,
|
||||
HoldsOrigin,
|
||||
HoldsByPass,
|
||||
Passed,
|
||||
PassedOrigin;
|
||||
|
||||
|
||||
fun hasBall() = this != None
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
enum class CourtType {
|
||||
Plain,
|
||||
Half
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
data class MovementPath(val items: List<ComponentId>)
|
@ -0,0 +1,13 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
import com.iqball.app.geo.Vector
|
||||
|
||||
data class PlayerInfo(
|
||||
val team: PlayerTeam,
|
||||
val role: String,
|
||||
val isPhantom: Boolean,
|
||||
val pos: Vector,
|
||||
val id: ComponentId,
|
||||
val actions: List<Action>,
|
||||
val ballState: BallState
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
enum class PlayerTeam {
|
||||
Allies,
|
||||
Opponents
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
import com.iqball.app.geo.Pos
|
||||
|
||||
sealed interface Positioning
|
||||
data class RelativePositioning(val attach: ComponentId) : Positioning
|
||||
data class FixedPosition(val x: Float, val y: Float) : Positioning {
|
||||
fun toPos() = Pos(x, y)
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
typealias ComponentId = String
|
||||
|
||||
|
||||
sealed interface StepComponent {
|
||||
val id: ComponentId
|
||||
val actions: List<Action>
|
||||
}
|
||||
|
||||
sealed interface PositionableComponent<P : Positioning> {
|
||||
val pos: P
|
||||
}
|
||||
|
||||
sealed interface PlayerLike : PositionableComponent<Positioning>, 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<Action>,
|
||||
) : 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<Action>
|
||||
) : PlayerLike, StepComponent
|
||||
|
||||
data class BallComponent(
|
||||
override val id: ComponentId,
|
||||
override val actions: List<Action>,
|
||||
override val pos: FixedPosition
|
||||
) : StepComponent, PositionableComponent<FixedPosition>
|
@ -0,0 +1,15 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
import java.lang.RuntimeException
|
||||
|
||||
data class StepContent(val components: List<StepComponent>) {
|
||||
inline fun <reified C: StepComponent> 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)
|
@ -0,0 +1,17 @@
|
||||
package com.iqball.app.model.tactic
|
||||
|
||||
data class StepNodeInfo(val id: Int, val children: List<StepNodeInfo>)
|
||||
|
||||
|
||||
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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.iqball.app.api
|
||||
package com.iqball.app.net
|
||||
|
||||
import arrow.core.Either
|
||||
import okhttp3.ResponseBody
|
@ -1,17 +1,13 @@
|
||||
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 {
|
||||
|
||||
@Serializable
|
||||
data class AuthResponse(val token: String, val expirationDate: String)
|
||||
data class AuthResponse(val token: String, val expirationDate: Long)
|
||||
|
||||
@Serializable
|
||||
data class RegisterRequest(val username: String, val email: String, val password: String)
|
@ -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<String, Array<String>>
|
||||
typealias APIResult<R> = Either<ErrorResponseResult, R>
|
||||
|
||||
interface IQBallService : AuthService, UserService
|
||||
interface IQBallService : AuthService, UserService, TacticService
|
@ -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<GetTacticInfoResponse>
|
||||
|
||||
data class GetTacticStepsTreeResponse(
|
||||
val root: StepNodeInfo
|
||||
)
|
||||
|
||||
@GET("tactics/{tacticId}/tree")
|
||||
suspend fun getTacticStepsTree(
|
||||
@Header("Authorization") auth: Token,
|
||||
@Path("tacticId") tacticId: Int
|
||||
): APIResult<GetTacticStepsTreeResponse>
|
||||
|
||||
|
||||
|
||||
@GET("tactics/{tacticId}/steps/{stepId}")
|
||||
suspend fun getTacticStepContent(
|
||||
@Header("Authorization") auth: Token,
|
||||
@Path("tacticId") tacticId: Int,
|
||||
@Path("stepId") stepId: Int
|
||||
): APIResult<StepContent>
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.iqball.app.api.service
|
||||
package com.iqball.app.net.service
|
||||
|
||||
import com.iqball.app.model.Tactic
|
||||
import com.iqball.app.model.Team
|
@ -1,9 +1,109 @@
|
||||
package com.iqball.app.page
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.net.service.AuthService
|
||||
import com.iqball.app.session.Authentication
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Composable
|
||||
fun LoginPage() {
|
||||
Text(text = "Login Page")
|
||||
fun LoginPage(
|
||||
service: AuthService,
|
||||
onLoginSuccess: (Authentication) -> Unit,
|
||||
onNavigateToRegister: () -> Unit
|
||||
) {
|
||||
var email by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var errors by remember { mutableStateOf("") }
|
||||
|
||||
Surface(
|
||||
color = Color.White,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "S'identifier",
|
||||
fontSize = 28.sp,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
Text(
|
||||
text = errors,
|
||||
color = Color.Red,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("Email") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.Black,
|
||||
unfocusedTextColor = Color.Black
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.Black,
|
||||
unfocusedTextColor = Color.Black
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = {
|
||||
runBlocking {
|
||||
Log.i("ZIZI", "On click wesh")
|
||||
when (val response = service.login(AuthService.LoginRequest(email, password))) {
|
||||
is Either.Left -> {
|
||||
errors = response.value.toList()
|
||||
.flatMap { entry -> entry.second.map { "${entry.first} : ${it}" } }
|
||||
.joinToString("\n")
|
||||
}
|
||||
|
||||
is Either.Right -> onLoginSuccess(
|
||||
Authentication(
|
||||
response.value.token,
|
||||
response.value.expirationDate.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(text = "Se connecter")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(onClick = { onNavigateToRegister() }) {
|
||||
Text(text = "Vous n'avez pas de compte ?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +1,130 @@
|
||||
package com.iqball.app.page
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.api.service.AuthService.RegisterRequest
|
||||
import com.iqball.app.api.service.IQBallService
|
||||
import com.iqball.app.net.service.AuthService
|
||||
import com.iqball.app.session.Authentication
|
||||
import com.iqball.app.session.MutableSession
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Composable
|
||||
fun RegisterPage(service: IQBallService) {
|
||||
fun RegisterPage(
|
||||
service: AuthService,
|
||||
onRegisterSuccess: (Authentication) -> Unit,
|
||||
onNavigateToLogin: () -> Unit
|
||||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var email by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var errors by remember { mutableStateOf("") }
|
||||
|
||||
Surface(
|
||||
color = Color.White,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
||||
var text by remember { mutableStateOf("No message !") }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "S'enregistrer",
|
||||
fontSize = 28.sp,
|
||||
color = Color.Black
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = errors,
|
||||
color = Color.Red,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Nom d'utilisateur") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.Black,
|
||||
unfocusedTextColor = Color.Black
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Mot de passe") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.Black,
|
||||
unfocusedTextColor = Color.Black
|
||||
),
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("Email") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.Black,
|
||||
unfocusedTextColor = Color.Black
|
||||
)
|
||||
)
|
||||
Button(onClick = {
|
||||
runBlocking {
|
||||
when (val response =
|
||||
service.register(AuthService.RegisterRequest(username, email, password))) {
|
||||
is Either.Left -> {
|
||||
errors = response.value.toList()
|
||||
.flatMap { entry -> entry.second.map { "${entry.first} : ${it}" } }
|
||||
.joinToString("\n")
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
val result = service.login(AuthService.LoginRequest("maxime@mail.com", "123456"))
|
||||
|
||||
when (result) {
|
||||
is Either.Left -> {
|
||||
println("Error : " + result.value)
|
||||
text = result.toString()
|
||||
}
|
||||
is Either.Right -> {
|
||||
val token = result.value.token
|
||||
val userDataResponse = service.getUserData(token)
|
||||
|
||||
when (userDataResponse) {
|
||||
is Either.Left -> println("Error User Data : " + userDataResponse.value)
|
||||
is Either.Right -> println("Success User Data : " + userDataResponse.value)
|
||||
is Either.Right -> {
|
||||
onRegisterSuccess(
|
||||
Authentication(
|
||||
response.value.token,
|
||||
response.value.expirationDate
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text = userDataResponse.toString()
|
||||
}) {
|
||||
Text(text = "Créer votre compte")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(onClick = { onNavigateToLogin() }) {
|
||||
Text(text = "Vous avez déjà un compte ?")
|
||||
}
|
||||
}
|
||||
|
||||
println(result)
|
||||
Log.i("%", result.toString())
|
||||
}
|
||||
|
||||
Text(text = text)
|
||||
}
|
||||
|
||||
|
@ -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<ComponentId, Offset>() }
|
||||
val parentOffsets =
|
||||
remember(selectedStepId) { mutableStateMapOf<ComponentId, Offset>() }
|
||||
|
||||
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<Boolean>) {
|
||||
|
||||
|
||||
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<String, VisualizerInitialData> {
|
||||
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))
|
||||
}
|
@ -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<Either<*, *>>() {
|
||||
|
||||
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<Any>()
|
||||
return null
|
||||
}
|
||||
else -> throw JsonDataException("unexpected token : $token")
|
||||
}
|
||||
|
||||
if (valueJsonType == leftJsonType) {
|
||||
val value = moshi.adapter<Any>(leftType).fromJson(reader)
|
||||
return Either.Left(value)
|
||||
}
|
||||
|
||||
if (valueJsonType == rightJsonType) {
|
||||
val value = moshi.adapter<Any>(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<out Annotation>,
|
||||
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
|
||||
}
|
@ -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<E : Enum<E>>(
|
||||
values: Map<String, E>,
|
||||
private val bindNames: Boolean,
|
||||
private val ignoreCase: Boolean,
|
||||
private val fallback: E?,
|
||||
private val clazz: Class<E>
|
||||
) : JsonAdapter<E>() {
|
||||
|
||||
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<E : Enum<E>>(
|
||||
private val values: Map<String, E>,
|
||||
private val bindNames: Boolean,
|
||||
private val ignoreCase: Boolean,
|
||||
private val fallback: E?,
|
||||
private val clazz: Class<E>
|
||||
) : JsonAdapter.Factory {
|
||||
override fun create(
|
||||
type: Type,
|
||||
annotations: MutableSet<out Annotation>,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<*>? {
|
||||
if (type.rawType != clazz)
|
||||
return null
|
||||
|
||||
return EnumTypeAdapter(values, bindNames, ignoreCase, fallback, clazz)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
class Builder<E : Enum<E>>(val values: MutableMap<String, E> = HashMap()) {
|
||||
infix fun String.means(e: E) {
|
||||
values[this] = e
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified E : Enum<E>> create(
|
||||
ignoreCase: Boolean = false,
|
||||
bindNames: Boolean = true,
|
||||
fallback: E? = null,
|
||||
build: Builder<E>.() -> Unit = {}
|
||||
): EnumTypeAdapterFactory<E> {
|
||||
val builder = Builder<E>()
|
||||
build(builder)
|
||||
return EnumTypeAdapterFactory(builder.values, bindNames, ignoreCase, fallback, E::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
package com.iqball.app.session
|
||||
typealias Token = String
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
data class Authentication(val token: String, val expirationDate: LocalDateTime)
|
||||
data class Authentication(val token: String, val expirationDate: Long)
|
||||
|
@ -0,0 +1,3 @@
|
||||
package com.iqball.app.session
|
||||
|
||||
data class DataSession(override val auth: Authentication? = null) : Session
|
@ -1,5 +0,0 @@
|
||||
package com.iqball.app.session
|
||||
|
||||
interface MutableSession : Session {
|
||||
override var auth: Authentication
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
package com.iqball.app.session
|
||||
|
||||
interface Session {
|
||||
val auth: Authentication
|
||||
val auth: Authentication?
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="1280dp"
|
||||
android:height="1276dp"
|
||||
android:viewportWidth="1280"
|
||||
android:viewportHeight="1276">
|
||||
<path
|
||||
android:pathData="M608.5,0.6c-112.4,6.6 -212.4,37.8 -305.5,95.2 -11.3,7 -15,9.8 -11.5,8.8 5,-1.4 35,-3.7 55.5,-4.3 92.5,-2.4 175.5,19.8 257.3,69.1 9.5,5.6 32.1,20.2 50.2,32.2 51.1,34 60,37.3 75.3,28 28.1,-17 48.1,-45.7 55.9,-80 2.4,-10.5 2.4,-39.3 -0.1,-50.6 -5.9,-27.7 -20.2,-55.7 -41.7,-81.3 -4.5,-5.4 -9.1,-10.2 -10.3,-10.6 -1.9,-0.8 -16,-2.5 -41.1,-5.1 -9.9,-1.1 -72.4,-2.1 -84,-1.4z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M763,12.5c0,0.2 2.8,4.8 6.3,10.2 21,32.4 33.5,62.6 37.8,91.3 1.6,11.2 1.4,31 -0.6,42.4 -5.6,32.8 -24.5,65.1 -59.1,101.4 -7.9,8.3 -10.5,11.7 -9.8,12.7 1.1,1.7 95.3,89.5 95.9,89.5 0.3,-0 13.6,-10.5 29.7,-23.3 108.5,-86.3 148.5,-114 206.5,-143.1 9.4,-4.7 17.3,-9.1 17.5,-9.8 0.7,-2 -23.2,-24 -46.7,-42.9 -76.6,-61.6 -165.4,-104.3 -262.2,-125.9 -13.5,-3.1 -15.3,-3.3 -15.3,-2.5z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M305.5,114.5c-12.7,1.3 -26.2,3.6 -35.9,6.1 -6.9,1.8 -15.7,8.1 -39.7,28 -98.4,82.3 -169.5,190.9 -204.9,313.5 -8.4,29.1 -14,55 -18.5,85.4 -1.9,13 -2.9,23.1 -2.3,22.5 0.3,-0.3 1.6,-3.4 2.8,-7 3.6,-10.3 12.4,-26.7 19.5,-36.3 38.6,-51.8 115.1,-90.1 247.5,-123.8 29.1,-7.4 55.3,-13.4 91,-20.9 10.7,-2.3 23.6,-5 28.5,-6 5,-1.1 15.8,-3.3 24,-5 8.3,-1.7 19.1,-3.9 24,-5 5,-1 13.3,-2.8 18.5,-3.9 107.9,-22.8 159.9,-39.2 191.5,-60.4 9.6,-6.4 20.5,-18.1 24.4,-26.2 2.8,-5.7 3.5,-8.2 3.5,-13.5 0.1,-6.2 -0.2,-7 -4.4,-13.5 -14.2,-21.8 -52.6,-49.5 -101.9,-73.7 -92,-44.9 -194.1,-68 -267.6,-60.3z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M1094,208.6c-6.3,4.1 -17.6,11.4 -25,16.2 -29.9,19.3 -79.8,54.7 -121.5,86.1 -42.3,31.8 -89.9,71.1 -89.3,73.6 0.2,0.6 18.9,24.6 41.7,53.2l41.4,52 17.1,-8.2c52.8,-25.5 97.5,-37.5 139.8,-37.5 57.4,-0 106,23.9 146.4,72 8.5,10.1 21.9,28.6 26.4,36.5 1.8,3.1 3.4,5.4 3.6,5.2 1.1,-1.1 -6.9,-45.6 -12,-67.1 -23.5,-97.6 -69.2,-187.5 -134.7,-264.6 -5.4,-6.3 -12.6,-14.5 -16.1,-18.2l-6.3,-6.6 -11.5,7.4z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M712.5,292.7c-7,6.2 -23.1,18.1 -32.7,24.2 -49.1,30.8 -112,53.1 -210.6,74.6 -9,2 -45.2,9.4 -80.4,16.5 -112.8,22.8 -149.1,31.7 -197.8,48.5 -93.6,32.3 -152.7,76.1 -186,137.8l-4.2,7.8 -0.5,25.2c-3.1,150.3 47.2,297.4 141.5,413.7 4.7,5.8 8.7,10.7 8.8,10.8 0.8,1.1 5,-3.5 21,-23.3 106.7,-131.8 204.2,-240.8 317.4,-354.9 95.3,-96 190.8,-184.6 293.3,-272.1 13.5,-11.6 24.6,-21.5 24.7,-22 0,-1 -89,-90.5 -90,-90.5 -0.3,0.1 -2.3,1.7 -4.5,3.7z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M825.5,414.2c-88.6,66.8 -153.4,121.6 -237,200.4 -102.8,96.9 -212.5,213.6 -329.9,350.9 -13.2,15.4 -26.5,30.9 -29.6,34.5 -14.2,16.4 -59.2,70.3 -59.6,71.5 -0.7,1.6 30,32.6 46.6,47.1 67.8,59.3 142.5,102 226,129.4 23.1,7.5 49,14.3 73,19 20.7,4 41.3,7.2 56,8.5 6.2,0.6 6.9,0.4 15.5,-3.8 19.2,-9.4 34.6,-20.9 51.5,-38.3 35.1,-36.3 60.1,-85.4 83.9,-164.9 5.8,-19.2 15.8,-58.7 21.1,-83.5 4,-18.5 11.4,-54.1 19,-92 9.8,-48.6 16.8,-82.5 20,-96.5 0.5,-2.2 1.6,-6.9 2.4,-10.5 0.8,-3.6 3.6,-15.1 6.1,-25.5 4,-16.4 5.9,-23.5 11.5,-43.5 5.2,-18.7 16.9,-53.2 24.2,-71.5 23.2,-58.2 56.2,-106.2 91.1,-132.5 2.5,-1.9 4.5,-3.8 4.4,-4.2 -0.2,-0.8 -83.8,-103.9 -84,-103.7 -0.1,-0 -5.6,4.1 -12.2,9.1z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M1057,466.7c-35.8,5.4 -57.9,14.9 -74.1,31.9 -8.4,8.9 -9.3,10.6 -8.6,16.8 1,10 6.5,19.9 39.7,72.1 36.8,57.8 50.8,81.8 69.1,118.5 37.9,76 56.7,141.3 61.1,213.2 2.4,39.2 -3,86.3 -14.2,124.3 -1.6,5.4 -2.8,10 -2.7,10.2 0.8,0.8 29.7,-36.9 41.9,-54.7 72.2,-105.5 110.9,-231.3 110.8,-360 0,-29.4 -1.5,-49 -4,-54.7 -3,-6.8 -12.7,-22.8 -19.4,-31.8 -24,-32.6 -59,-58.1 -99.9,-72.9 -27.1,-9.8 -49.8,-13.7 -79.1,-13.5 -10,0.1 -19.2,0.3 -20.6,0.6z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M926.5,544.3c-29.2,9.7 -54.1,40.3 -74.4,91.4 -17.4,43.9 -31.2,96.4 -52.1,197.5 -1.6,8.2 -4.4,21.4 -6,29.5 -1.7,8.2 -3.9,18.8 -5,23.8 -1.1,4.9 -3.3,15.7 -5,24 -38.9,189.3 -79.8,291.5 -139,347.8 -5.7,5.4 -12.3,11.2 -14.7,13l-4.5,3.2 20.3,0.3c56.8,0.9 124.7,-9.5 184.4,-28.3 92.8,-29.2 174.1,-76.5 245.1,-142.3 17.9,-16.6 21.6,-20.7 25.3,-28.1 9.6,-19 16.3,-41.3 20.2,-67.6 2,-12.8 2.3,-19.2 2.3,-43 0,-34.4 -2.2,-56 -9.5,-92.7 -19.1,-95.9 -69.6,-210.9 -124.9,-284.3 -17.5,-23.1 -35.1,-41 -42.8,-43.3 -6.4,-1.9 -15.3,-2.3 -19.7,-0.9z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
@ -0,0 +1,171 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="270"
|
||||
android:viewportHeight="270">
|
||||
<path
|
||||
android:pathData="M24,236L24,26"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M248,25L248,236"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M249,237L23,237"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M23,25.5L247,25.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M108.5,26C108.5,33.03 111.35,39.77 116.41,44.74C121.48,49.71 128.34,52.5 135.5,52.5C142.66,52.5 149.52,49.71 154.59,44.74C159.65,39.77 162.5,33.03 162.5,26"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M99.5,236L99.5,151"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M100.5,236L100.5,151"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M172,150.5H100"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M99.5,151V150"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M172.5,151V150"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M172.5,151L172.5,236"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M171.5,151L171.5,236"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M37.5,236L37.5,170"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M233.5,236L233.5,170"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M163,150.5C163,147.02 162.32,143.57 160.98,140.36C159.65,137.14 157.7,134.22 155.24,131.76C152.78,129.3 149.86,127.35 146.64,126.02C143.43,124.68 139.98,124 136.5,124C133.02,124 129.57,124.68 126.36,126.02C123.14,127.35 120.22,129.3 117.76,131.76C115.3,134.22 113.35,137.14 112.02,140.36C110.68,143.57 110,147.02 110,150.5L136.5,150.5L163,150.5Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M110,150.5C110,153.98 110.68,157.43 112.02,160.64C113.35,163.86 115.3,166.78 117.76,169.24C120.22,171.7 123.14,173.65 126.36,174.98C129.57,176.32 133.02,177 136.5,177C139.98,177 143.43,176.32 146.64,174.98C149.86,173.65 152.78,171.7 155.24,169.24C157.7,166.78 159.65,163.86 160.98,160.64C162.32,157.43 163,153.98 163,150.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M177,173.5L172,173.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M177,185.5L172,185.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M177,197.5L172,197.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M177,209.5L172,209.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M100,173.5L95,173.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M100,185.5L95,185.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M100,197.5L95,197.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M100,209.5L95,209.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M233.61,168.79C225.61,149.95 211.97,133.85 194.43,122.57C176.9,111.29 156.27,105.35 135.21,105.5C114.15,105.65 93.62,111.9 76.26,123.43C58.9,134.96 45.51,151.24 37.81,170.2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M233.5,169V168"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M233.5,170V169"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M118.5,218.5C118.5,214 120.34,209.67 123.62,206.48C126.9,203.29 131.35,201.5 136,201.5C140.65,201.5 145.1,203.29 148.38,206.48C151.66,209.67 153.5,214 153.5,218.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M136.5,221.5m0,3a3,3 0,1 1,0 -6a3,3 0,1 1,0 6"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M149,225.5L123,225.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M37.5,171V170"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M38.15,169.77C38.12,169.78 38.14,169.9 38.13,169.93C38.13,170.02 38.07,170.11 38.07,170.21C38.07,170.24 38.04,170.33 38.03,170.37C38.01,170.42 37.97,170.48 37.97,170.54C37.96,170.6 37.91,170.63 37.89,170.69C37.87,170.73 37.84,170.8 37.81,170.82C37.78,170.84 37.76,170.94 37.74,170.97C37.69,171.04 37.63,171.12 37.59,171.19"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M37.99,169.07C37.99,169.09 37.99,169.11 37.99,169.13C37.99,169.16 37.98,169.19 37.96,169.21C37.89,169.32 37.8,169.4 37.72,169.5C37.62,169.61 37.51,169.73 37.45,169.87C37.42,169.95 37.39,170.04 37.36,170.12C37.35,170.15 37.31,170.21 37.33,170.25"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
@ -0,0 +1,301 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="567dp"
|
||||
android:height="269dp"
|
||||
android:viewportWidth="567"
|
||||
android:viewportHeight="269">
|
||||
<path
|
||||
android:pathData="M73,24L495,24"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M494,23L494,247"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M495,248L73,248"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M72,249L72,23"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M283.5,23L283.5,247"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M283.5,135.5m-27,0a27,27 0,1 1,54 0a27,27 0,1 1,-54 0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M73,99.5L158,99.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M73,100.5L158,100.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M158.5,172V100"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M158,99.5H159"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M158,172.5H159"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M158,172.5L73,172.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M158,171.5L73,171.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M73,37.5L139,37.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M73,233.5L139,233.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M158.5,163C161.98,163 165.43,162.32 168.64,160.98C171.86,159.65 174.78,157.7 177.24,155.24C179.7,152.78 181.65,149.86 182.98,146.64C184.32,143.43 185,139.98 185,136.5C185,133.02 184.32,129.57 182.98,126.36C181.65,123.14 179.7,120.22 177.24,117.76C174.78,115.3 171.86,113.35 168.64,112.02C165.43,110.68 161.98,110 158.5,110L158.5,136.5L158.5,163Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M158.5,110C155.02,110 151.57,110.68 148.36,112.02C145.14,113.35 142.22,115.3 139.76,117.76C137.3,120.22 135.35,123.14 134.02,126.36C132.68,129.57 132,133.02 132,136.5C132,139.98 132.68,143.43 134.02,146.64C135.35,149.86 137.3,152.78 139.76,155.24C142.22,157.7 145.14,159.65 148.36,160.98C151.57,162.32 155.02,163 158.5,163"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M135.5,177L135.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M123.5,177L123.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M111.5,177L111.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M99.5,177L99.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M135.5,100L135.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M123.5,100L123.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M111.5,100L111.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M99.5,100L99.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M140.21,233.61C159.05,225.61 175.15,211.97 186.43,194.43C197.71,176.9 203.65,156.27 203.5,135.21C203.35,114.15 197.1,93.62 185.57,76.26C174.04,58.9 157.76,45.51 138.8,37.81"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M140,233.5H141"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M139,233.5H140"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M90.5,118.5C95,118.5 99.33,120.34 102.52,123.62C105.71,126.9 107.5,131.35 107.5,136C107.5,140.65 105.71,145.1 102.52,148.38C99.33,151.66 95,153.5 90.5,153.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M87.5,136.5m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M83.5,149L83.5,123"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M494,99.5L409,99.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M494,100.5L409,100.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M408.5,172V100"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M409,99.5H408"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M409,172.5H408"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M409,172.5L494,172.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M409,171.5L494,171.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M494,37.5L428,37.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M494,233.5L428,233.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M408.5,163C405.02,163 401.57,162.32 398.36,160.98C395.14,159.65 392.22,157.7 389.76,155.24C387.3,152.78 385.35,149.86 384.02,146.64C382.68,143.43 382,139.98 382,136.5C382,133.02 382.68,129.57 384.02,126.36C385.35,123.14 387.3,120.22 389.76,117.76C392.22,115.3 395.14,113.35 398.36,112.02C401.57,110.68 405.02,110 408.5,110L408.5,136.5L408.5,163Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M408.5,110C411.98,110 415.43,110.68 418.64,112.02C421.86,113.35 424.78,115.3 427.24,117.76C429.7,120.22 431.65,123.14 432.98,126.36C434.32,129.57 435,133.02 435,136.5C435,139.98 434.32,143.43 432.98,146.64C431.65,149.86 429.7,152.78 427.24,155.24C424.78,157.7 421.86,159.65 418.64,160.98C415.43,162.32 411.98,163 408.5,163"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M431.5,177L431.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M443.5,177L443.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M455.5,177L455.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M467.5,177L467.5,172"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M431.5,100L431.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M443.5,100L443.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M455.5,100L455.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M467.5,100L467.5,95"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M426.79,233.61C407.95,225.61 391.85,211.97 380.57,194.43C369.3,176.9 363.35,156.27 363.5,135.21C363.65,114.15 369.9,93.62 381.43,76.26C392.95,58.9 409.24,45.51 428.2,37.81"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M427,233.5H426"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M428,233.5H427"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M476.5,118.5C472,118.5 467.67,120.34 464.48,123.62C461.29,126.9 459.5,131.35 459.5,136C459.5,140.65 461.29,145.1 464.48,148.38C467.67,151.66 472,153.5 476.5,153.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M479.5,136.5m3,0a3,3 0,1 0,-6 0a3,3 0,1 0,6 0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M483.5,149L483.5,123"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M138,37.5H139"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M139.23,38.15C139.22,38.12 139.1,38.14 139.07,38.13C138.98,38.13 138.89,38.07 138.79,38.07C138.76,38.07 138.67,38.04 138.63,38.03C138.58,38.01 138.52,37.97 138.46,37.97C138.4,37.96 138.37,37.91 138.31,37.89C138.27,37.87 138.2,37.84 138.18,37.81C138.16,37.78 138.06,37.76 138.03,37.74C137.96,37.69 137.88,37.63 137.81,37.59"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M139.93,37.99C139.91,37.99 139.89,37.99 139.87,37.99C139.84,37.99 139.81,37.98 139.79,37.96C139.68,37.89 139.6,37.8 139.5,37.72C139.39,37.62 139.27,37.51 139.13,37.45C139.05,37.42 138.96,37.39 138.88,37.36C138.85,37.35 138.79,37.31 138.75,37.33"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
After Width: | Height: | Size: 5.6 KiB |
Loading…
Reference in new issue