@ -0,0 +1,10 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
@ -0,0 +1,2 @@
|
||||
# IQBall - Android Application
|
||||
This repository hosts the IQBall application for the android platform
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,90 @@
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
|
||||
kotlin("plugin.serialization") version "1.9.23"
|
||||
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.iqball.app"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.iqball.app"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
isDebuggable = true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit2.kotlinx.serialization.converter)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.moshi.adapters)
|
||||
implementation(libs.converter.moshi.v250)
|
||||
implementation(libs.moshi.kotlin)
|
||||
implementation(libs.zoomable)
|
||||
implementation(libs.compose.free.scroll)
|
||||
|
||||
implementation(libs.retrofit.adapters.arrow)
|
||||
implementation(libs.arrow.core)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
}
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 6.0 KiB |
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,24 @@
|
||||
package com.iqball.app
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.iqball.app", appContext.packageName)
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.IQBall"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.IQBall">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
@ -0,0 +1,140 @@
|
||||
package com.iqball.app
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.iqball.app.model.tactic.ActionType
|
||||
import com.iqball.app.model.tactic.BallComponent
|
||||
import com.iqball.app.model.tactic.BallState
|
||||
import com.iqball.app.model.tactic.FixedPosition
|
||||
import com.iqball.app.model.tactic.PhantomComponent
|
||||
import com.iqball.app.model.tactic.PlayerComponent
|
||||
import com.iqball.app.model.tactic.PlayerTeam
|
||||
import com.iqball.app.model.tactic.Positioning
|
||||
import com.iqball.app.model.tactic.RelativePositioning
|
||||
import com.iqball.app.model.tactic.StepComponent
|
||||
import com.iqball.app.net.EitherBodyConverter
|
||||
import com.iqball.app.net.EitherCallAdapterFactory
|
||||
import com.iqball.app.net.service.IQBallService
|
||||
import com.iqball.app.page.HomePage
|
||||
import com.iqball.app.page.LoginPage
|
||||
import com.iqball.app.page.RegisterPage
|
||||
import com.iqball.app.serialization.EitherTypeAdapterFactory
|
||||
import com.iqball.app.serialization.EnumTypeAdapterFactory
|
||||
import com.iqball.app.session.DataSession
|
||||
import com.iqball.app.session.Session
|
||||
import com.iqball.app.ui.theme.IQBallTheme
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.create
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val moshi = Moshi.Builder()
|
||||
.add(EitherTypeAdapterFactory)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(StepComponent::class.java, "type")
|
||||
.withSubtype(PlayerComponent::class.java, "player")
|
||||
.withSubtype(PhantomComponent::class.java, "phantom")
|
||||
.withSubtype(BallComponent::class.java, "ball")
|
||||
)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Positioning::class.java, "type")
|
||||
.withSubtype(FixedPosition::class.java, "fixed")
|
||||
.withSubtype(RelativePositioning::class.java, "follows")
|
||||
)
|
||||
.add(EnumTypeAdapterFactory.create<PlayerTeam>(true))
|
||||
.add(EnumTypeAdapterFactory.create<BallState>(true) {
|
||||
"HOLDS_ORIGIN" means BallState.HoldsOrigin
|
||||
"HOLDS_BY_PASS" means BallState.HoldsByPass
|
||||
"PASSED_ORIGIN" means BallState.PassedOrigin
|
||||
})
|
||||
.add(EnumTypeAdapterFactory.create<ActionType>(true))
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.addConverterFactory(EitherBodyConverter.create())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(EitherCallAdapterFactory.create())
|
||||
.baseUrl("https://iqball.maxou.dev/api/dotnet-master/")
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor { it.proceed(it.request()) }
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
val service = retrofit.create<IQBallService>()
|
||||
|
||||
setContent {
|
||||
IQBallTheme(darkTheme = false, dynamicColor = false) {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
val sessionState = remember { mutableStateOf<Session>(DataSession()) }
|
||||
App(service, sessionState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun App(service: IQBallService, sessionState: MutableState<Session>) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = "login") {
|
||||
composable("login") {
|
||||
LoginPage(
|
||||
service = service,
|
||||
onLoginSuccess = { auth ->
|
||||
Log.i("ZIZI", "auth : ${auth}")
|
||||
sessionState.value = DataSession(auth)
|
||||
navController.navigate("home")
|
||||
Log.i("ZIZI", "auth : ${auth}")
|
||||
},
|
||||
onNavigateToRegister = {
|
||||
navController.navigate("register")
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("register") {
|
||||
RegisterPage(
|
||||
service = service,
|
||||
onRegisterSuccess = { auth ->
|
||||
sessionState.value = DataSession(auth)
|
||||
navController.navigate("home")
|
||||
},
|
||||
onNavigateToLogin = {
|
||||
navController.navigate("login")
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("home") {
|
||||
HomePage(service = service, sessionState.value.auth!!)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,9 @@
|
||||
package com.iqball.app.model
|
||||
|
||||
data class Tactic (
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val ownerId: Int,
|
||||
val courtType: String,
|
||||
val creationDate: Long
|
||||
)
|
@ -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,9 @@
|
||||
package com.iqball.app.model;
|
||||
|
||||
data class Team (
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val picture: String,
|
||||
val mainColor: String,
|
||||
val secondColor: String
|
||||
)
|
@ -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
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.iqball.app.net
|
||||
|
||||
import arrow.core.Either
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class EitherBodyConverter : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<ResponseBody, *>? {
|
||||
|
||||
if (type !is ParameterizedType) {
|
||||
return null
|
||||
}
|
||||
if (type.rawType != Call::class.java) {
|
||||
return null
|
||||
}
|
||||
val eitherType = type.actualTypeArguments.first()
|
||||
if (eitherType !is ParameterizedType || eitherType.rawType != Either::class.java) {
|
||||
return null
|
||||
}
|
||||
|
||||
val eitherRightType = eitherType.actualTypeArguments[1]
|
||||
return retrofit.nextResponseBodyConverter<Any>(this, eitherRightType, annotations)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create() = EitherBodyConverter()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.iqball.app.net
|
||||
|
||||
import arrow.core.Either
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
|
||||
class EitherCall<L, R>(
|
||||
private val retrofit: Retrofit,
|
||||
private val eitherType: ParameterizedType,
|
||||
private val delegate: Call<Any>
|
||||
) : Call<Either<L, R>> {
|
||||
override fun enqueue(callback: Callback<Either<L, R>>) {
|
||||
delegate.enqueue(object : Callback<Any> {
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
val result: Either<L, R> = if (response.isSuccessful) {
|
||||
Either.Right(response.body()!! as R)
|
||||
} else {
|
||||
val leftType = eitherType.actualTypeArguments[0]
|
||||
val converter = retrofit.nextResponseBodyConverter<L>(null, leftType, arrayOf())
|
||||
val result = converter.convert(response.errorBody()!!)!!
|
||||
Either.Left(result)
|
||||
}
|
||||
callback.onResponse(this@EitherCall, Response.success(result))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
callback.onFailure(this@EitherCall, t)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
override fun clone(): Call<Either<L, R>> = EitherCall(retrofit, eitherType, delegate.clone())
|
||||
|
||||
override fun execute(): Response<Either<L, R>> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun isExecuted(): Boolean = delegate.isExecuted
|
||||
|
||||
override fun cancel() = delegate.cancel()
|
||||
|
||||
override fun isCanceled(): Boolean = delegate.isCanceled
|
||||
|
||||
override fun request(): Request = delegate.request()
|
||||
|
||||
override fun timeout(): Timeout = delegate.timeout()
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.iqball.app.net
|
||||
|
||||
import arrow.core.Either
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class EitherCallAdapterFactory : CallAdapter.Factory() {
|
||||
override fun get(
|
||||
returnType: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): CallAdapter<*, *>? {
|
||||
if (returnType !is ParameterizedType) {
|
||||
return null
|
||||
}
|
||||
if (returnType.rawType != Call::class.java) {
|
||||
return null
|
||||
}
|
||||
val eitherType = returnType.actualTypeArguments.first()
|
||||
if (eitherType !is ParameterizedType || eitherType.rawType != Either::class.java) {
|
||||
return null
|
||||
}
|
||||
return object : CallAdapter<Any, Any> {
|
||||
override fun responseType(): Type = returnType
|
||||
|
||||
override fun adapt(call: Call<Any>): EitherCall<Any, Any> {
|
||||
return EitherCall(retrofit, eitherType, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create() = EitherCallAdapterFactory()
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.iqball.app.net.service
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AuthService {
|
||||
|
||||
@Serializable
|
||||
data class AuthResponse(val token: String, val expirationDate: Long)
|
||||
|
||||
@Serializable
|
||||
data class RegisterRequest(val username: String, val email: String, val password: String)
|
||||
|
||||
@POST("auth/register")
|
||||
suspend fun register(@Body req: RegisterRequest): APIResult<AuthResponse>
|
||||
|
||||
data class LoginRequest(val email: String, val password: String)
|
||||
|
||||
@POST("auth/token")
|
||||
suspend fun login(@Body req: LoginRequest): APIResult<AuthResponse>
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.iqball.app.net.service
|
||||
|
||||
import arrow.core.Either
|
||||
|
||||
typealias ErrorResponseResult = Map<String, Array<String>>
|
||||
typealias APIResult<R> = Either<ErrorResponseResult, R>
|
||||
|
||||
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>
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.iqball.app.net.service
|
||||
|
||||
import com.iqball.app.model.Tactic
|
||||
import com.iqball.app.model.Team
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
|
||||
interface UserService {
|
||||
data class UserDataResponse(val teams: List<Team>, val tactics: List<Tactic>)
|
||||
|
||||
@GET("user-data")
|
||||
suspend fun getUserData(@Header("Authorization") auth: String): APIResult<UserDataResponse>
|
||||
}
|
@ -0,0 +1,407 @@
|
||||
package com.iqball.app.page
|
||||
|
||||
import com.iqball.app.model.Tactic
|
||||
import com.iqball.app.model.Team
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults.pinnedScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import arrow.core.Either
|
||||
import coil.compose.AsyncImage
|
||||
import com.iqball.app.net.service.AuthService
|
||||
import com.iqball.app.net.service.IQBallService
|
||||
import com.iqball.app.net.service.UserService
|
||||
import com.iqball.app.session.Authentication
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomePage(service: IQBallService, auth: Authentication) {
|
||||
val tactics: List<Tactic>
|
||||
val teams: List<Team>
|
||||
var invalid = false
|
||||
|
||||
val data = getDataFromApi(service, auth)
|
||||
if (data == null) {
|
||||
tactics = listOf<Tactic>()
|
||||
teams = listOf<Team>()
|
||||
invalid = true
|
||||
|
||||
} else {
|
||||
tactics = data.tactics
|
||||
teams = data.teams
|
||||
}
|
||||
val scrollBehavior = pinnedScrollBehavior(rememberTopAppBarState())
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.secondary,
|
||||
),
|
||||
title = {
|
||||
Text(
|
||||
"IQBall",
|
||||
fontSize = 40.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Body(innerPadding, tactics, teams, invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Body(
|
||||
padding: PaddingValues,
|
||||
tactics: List<Tactic>,
|
||||
teams: List<Team>,
|
||||
invalid: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
) {
|
||||
val selectedTab = remember { mutableIntStateOf(0) }
|
||||
val tabs = listOf<Pair<String, @Composable () -> Unit>>(
|
||||
Pair("Espace personnel") {
|
||||
ListComponentCard(tactics) { tactic ->
|
||||
TacticCard(tactic = tactic)
|
||||
}
|
||||
},
|
||||
Pair("Mes équipes") {
|
||||
ListComponentCard(teams) { team ->
|
||||
TeamCard(team = team)
|
||||
}
|
||||
}
|
||||
)
|
||||
TabsSelector(tabsTitles = tabs.map { it.first }, selectedIndex = selectedTab)
|
||||
if (!invalid) {
|
||||
tabs[selectedTab.intValue].second()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
TextCentered(
|
||||
text = "Erreur : Aucune connexion internet. Veillez activer votre connexion internet puis relancer l'application",
|
||||
fontSize = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabsSelector(tabsTitles: List<String>, selectedIndex: MutableState<Int>) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 20.dp, start = 2.dp, end = 2.dp, bottom = 10.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
|
||||
for ((idx, tab) in tabsTitles.withIndex()) {
|
||||
TabButton(
|
||||
tab,
|
||||
fill = idx == selectedIndex.value,
|
||||
onClick = { selectedIndex.value = idx }
|
||||
)
|
||||
if (idx != tabsTitles.size - 1) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabButton(title: String, fill: Boolean, onClick: () -> Unit) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
Button(
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
color = scheme.tertiary
|
||||
),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (fill) scheme.tertiary else scheme.background,
|
||||
contentColor = if (fill) scheme.background else scheme.tertiary,
|
||||
),
|
||||
onClick = onClick
|
||||
) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <C> ListComponentCard(items: List<C>, componentCard: @Composable (C) -> Unit) {
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Fixed(2),
|
||||
modifier = Modifier
|
||||
.padding(5.dp),
|
||||
content = {
|
||||
items(items) { tactic ->
|
||||
componentCard(tactic)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TacticCard(tactic: Tactic) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.border(
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.shadow(1.dp, shape = RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
color = Color.White
|
||||
)
|
||||
.padding(15.dp)
|
||||
) {
|
||||
Row {
|
||||
TextCentered(text = tactic.name, fontSize = 16.sp)
|
||||
}
|
||||
Row {
|
||||
val date = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(tactic.creationDate),
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
val dateFormatted = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy kk:mm"))
|
||||
TextCentered(
|
||||
text = dateFormatted,
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TeamCard(team: Team) {
|
||||
var mainColor = Color.White
|
||||
var secondColor = Color.White
|
||||
var validMain = true
|
||||
var validSecond = true
|
||||
try {
|
||||
mainColor = Color(team.mainColor.toColorInt())
|
||||
} catch (e: Exception) {
|
||||
validMain = false
|
||||
}
|
||||
try {
|
||||
secondColor = Color(team.secondColor.toColorInt())
|
||||
} catch (e: Exception) {
|
||||
validSecond = false
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(5.dp)
|
||||
.border(
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.shadow(1.dp, shape = RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
color = Color.White
|
||||
)
|
||||
.padding(15.dp)
|
||||
|
||||
) {
|
||||
AsyncImage(
|
||||
model = team.picture,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
shape = RectangleShape
|
||||
)
|
||||
)
|
||||
TextCentered(text = team.name)
|
||||
Row {
|
||||
TeamColorCard("Couleur principale", mainColor, 0.5f)
|
||||
TeamColorCard("Couleur secondaire", secondColor)
|
||||
}
|
||||
if (!validMain || !validSecond) {
|
||||
TextCentered(text = "Erreur : Format des couleurs invalides", fontSize = 16.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TeamColorCard(text: String, color: Color, fraction: Float = 1f) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(2.dp)
|
||||
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier.size(30.dp),
|
||||
onDraw = {
|
||||
drawCircle(color = color)
|
||||
}
|
||||
)
|
||||
}
|
||||
TextCentered(text = text, fontSize = 6.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextCentered(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
fontSize: TextUnit = 18.sp,
|
||||
fontWeight: FontWeight? = null
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = fontSize,
|
||||
fontWeight = fontWeight
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDataFromApi(
|
||||
service: IQBallService,
|
||||
auth: Authentication
|
||||
): UserService.UserDataResponse? {
|
||||
var res: UserService.UserDataResponse? = null
|
||||
try {
|
||||
runBlocking {
|
||||
val data = service.getUserData(auth.token)
|
||||
when (data) {
|
||||
is Either.Left -> null
|
||||
is Either.Right -> {
|
||||
res = data.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
} catch (error: Exception) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
=======================================================
|
||||
Comment
|
||||
=======================================================
|
||||
|
||||
Managing lists to display with pairs might not be the best thing to do in the context of composable.
|
||||
We chose to stick with this model due to a lack of time.
|
||||
|
||||
We could have also done something like this:
|
||||
|
||||
@Composable
|
||||
fun Body(padding: PaddingValues, tactics: List<Tactic>, teams: List<Team>, invalid: Boolean) {
|
||||
Column(...) {
|
||||
val selectedTab by remember { mutableIntStateOf(0) }
|
||||
val tabs = remember(selectedTab) {
|
||||
mutableStateOf(TabsGroup.entries.getOrNull(selectedTab))
|
||||
}
|
||||
TabsSelector(tabsTitles = TabsGroup.entries.map { it.title }, selectedIndex = selectedTab, )
|
||||
if (!invalid) {
|
||||
ListComponentCard {
|
||||
when(selectedTab) {
|
||||
TabsGroup.TEAM -> {
|
||||
items(tactics) {
|
||||
TacticCard(tactic = it)
|
||||
}
|
||||
}
|
||||
TabsGroup.TACTIC -> ...
|
||||
}
|
||||
}
|
||||
tabs[selectedTab.intValue].second()
|
||||
return
|
||||
}
|
||||
|
||||
TextCentered(...)
|
||||
}
|
||||
}
|
||||
|
||||
enum class TabsGroup {
|
||||
TEAM,
|
||||
TACTIC
|
||||
}
|
||||
|
||||
val TabsGroup.title: String
|
||||
@Composable
|
||||
get() = when(this) {
|
||||
TabsGroup.TACTIC -> "Espace personnel"
|
||||
TabsGroup.TEAM -> "Mes équipes"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListComponentCard(componentCard: LazyStaggeredGridScope.() -> Unit) {
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Fixed(2),
|
||||
modifier = ...,
|
||||
content = {
|
||||
componentCard()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
*/
|
@ -0,0 +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(
|
||||
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 ?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package com.iqball.app.page
|
||||
|
||||
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.net.service.AuthService
|
||||
import com.iqball.app.session.Authentication
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Composable
|
||||
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()
|
||||
|
||||
) {
|
||||
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")
|
||||
}
|
||||
|
||||
is Either.Right -> {
|
||||
onRegisterSuccess(
|
||||
Authentication(
|
||||
response.value.token,
|
||||
response.value.expirationDate
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(text = "Créer votre compte")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(onClick = { onNavigateToLogin() }) {
|
||||
Text(text = "Vous avez déjà un compte ?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package com.iqball.app.session
|
||||
typealias Token = String
|
||||
|
||||
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
|
@ -0,0 +1,5 @@
|
||||
package com.iqball.app.session
|
||||
|
||||
interface Session {
|
||||
val auth: Authentication?
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package com.iqball.app.stub
|
||||
|
||||
import com.iqball.app.model.Tactic
|
||||
import com.iqball.app.model.Team
|
||||
|
||||
|
||||
private fun getStubTeam(): ArrayList<Team> {
|
||||
val teams = ArrayList<Team>()
|
||||
teams.addAll(
|
||||
listOf(
|
||||
Team(
|
||||
1,
|
||||
"equipe1",
|
||||
"https://www.shutterstock.com/image-vector/batman-logo-icon-vector-template-600nw-1998917738.jpg",
|
||||
"#4500FF",
|
||||
"#456789"
|
||||
),
|
||||
Team(
|
||||
2,
|
||||
"equipe2",
|
||||
"https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/2f899b52-daf8-4098-83fe-5c5e27b69915/d4s4nzj-5f915488-7462-4908-b3c5-1605b0e4dc32.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzJmODk5YjUyLWRhZjgtNDA5OC04M2ZlLTVjNWUyN2I2OTkxNVwvZDRzNG56ai01ZjkxNTQ4OC03NDYyLTQ5MDgtYjNjNS0xNjA1YjBlNGRjMzIuanBnIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.KqdQgobH9kzyMIeYIneNdyWgKTpGbztwSKqK5pO3YYs",
|
||||
"121212",
|
||||
"#564738"
|
||||
),
|
||||
Team(
|
||||
3,
|
||||
"equipe3",
|
||||
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ1jiizrhhGsr48WrxxBbDpkFrRKeAYlGgcNQ&usqp=CAU",
|
||||
"#987654",
|
||||
"121212"
|
||||
),
|
||||
Team(
|
||||
4,
|
||||
"equipe4",
|
||||
"https://www.shutterstock.com/image-vector/batman-logo-icon-vector-template-600nw-1998917738.jpg",
|
||||
"121212",
|
||||
"121212"
|
||||
)
|
||||
)
|
||||
)
|
||||
return teams
|
||||
}
|
||||
|
||||
private fun getStubTactic(): ArrayList<Tactic> {
|
||||
val tactics = ArrayList<Tactic>()
|
||||
tactics.addAll(
|
||||
listOf(
|
||||
Tactic(1, "Test", 1, "testType", 1),
|
||||
Tactic(2, "Test2", 1, "testType", 1),
|
||||
Tactic(3, "Test3", 4, "test23Type", 1),
|
||||
Tactic(3, "Test6", 4, "test23Type", 1),
|
||||
Tactic(1, "Test", 1, "testType", 1),
|
||||
Tactic(2, "Test2", 1, "testType", 1),
|
||||
Tactic(3, "Test3", 4, "test23Type", 1),
|
||||
Tactic(3, "Test6", 4, "test23Type", 1),
|
||||
Tactic(1, "Test", 1, "testType", 1),
|
||||
Tactic(2, "Test2", 1, "testType", 1),
|
||||
Tactic(3, "Test3", 4, "test23Type", 1),
|
||||
Tactic(3, "Test6", 4, "test23Type", 1),
|
||||
Tactic(1, "Test", 1, "testType", 1),
|
||||
Tactic(2, "Test2", 1, "testType", 1),
|
||||
Tactic(3, "Test3", 4, "test23Type", 1),
|
||||
Tactic(3, "Test6", 4, "test23Type", 1),
|
||||
Tactic(1, "Test", 1, "testType", 1),
|
||||
Tactic(2, "Test2", 1, "testType", 1),
|
||||
Tactic(3, "Test3", 4, "test23Type", 1),
|
||||
Tactic(3, "Test6", 4, "test23Type", 1)
|
||||
)
|
||||
)
|
||||
|
||||
return tactics
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.iqball.app.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
||||
val black = Color(0xFF191A21)
|
||||
val orange = Color(0xFFFFA239)
|
||||
val blue = Color(0xFF0D6EFD)
|
||||
val grey = Color(0xFF282A36)
|
||||
val back = Color(0xFFf8f8f8)
|
||||
val borderCard = Color(0xFFDADCE0)
|
||||
|
||||
val Allies = Color(0xFF64e4f5)
|
||||
val Opponents = Color(0xFFf59264)
|
||||
val BallColor = Color(0XFFc5520d)
|
||||
|
||||
val StepNode = Color(0xFF2AC008)
|
||||
val SelectedStepNode = Color(0xFF213519)
|
@ -0,0 +1,76 @@
|
||||
package com.iqball.app.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80,
|
||||
primaryContainer = black
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = black,
|
||||
secondary = orange,
|
||||
tertiary = blue,
|
||||
primaryContainer = black,
|
||||
surface = grey,
|
||||
background = back,
|
||||
outline = borderCard
|
||||
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun IQBallTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.primary.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.iqball.app.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
@ -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,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</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 |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 982 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 7.6 KiB |
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="border">#FFDADCE0</color>
|
||||
</resources>
|
@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">IQBall</string>
|
||||
<string name="espace_personnel">Espace Personnel</string>
|
||||
</resources>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.IQBall" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
@ -0,0 +1,17 @@
|
||||
package com.iqball.app
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication) apply false
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
@ -0,0 +1,60 @@
|
||||
[versions]
|
||||
agp = "8.2.2"
|
||||
arrowCore = "1.2.1"
|
||||
composeFreeScroll = "0.2.2"
|
||||
converterGson = "2.9.0"
|
||||
converterMoshi = "2.5.0"
|
||||
converterMoshiVersion = "2.5.0"
|
||||
graphicsShapes = "1.0.0-alpha05"
|
||||
kotlin = "1.9.0"
|
||||
coreKtx = "1.12.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
kotlinxDatetime = "0.3.2"
|
||||
kotlinxSerializationJsonJvm = "1.6.3"
|
||||
lifecycleRuntimeKtx = "2.7.0"
|
||||
activityCompose = "1.8.2"
|
||||
composeBom = "2024.02.02"
|
||||
moshi = "1.15.1"
|
||||
moshiAdapters = "1.15.1"
|
||||
moshiKotlin = "1.15.1"
|
||||
retrofit = "2.9.0"
|
||||
retrofit2KotlinxSerializationConverter = "1.0.0"
|
||||
retrofitAdaptersArrow = "1.0.9"
|
||||
navigationCompose = "2.7.7"
|
||||
zoomable = "1.6.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-graphics-shapes = { module = "androidx.graphics:graphics-shapes", version.ref = "graphicsShapes" }
|
||||
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" }
|
||||
compose-free-scroll = { module = "com.github.chihsuanwu:compose-free-scroll", version.ref = "composeFreeScroll" }
|
||||
converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshiAdapters" }
|
||||
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" }
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
retrofit-adapters-arrow = { module = "com.github.skydoves:retrofit-adapters-arrow", version.ref = "retrofitAdaptersArrow" }
|
||||
retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
@ -0,0 +1,6 @@
|
||||
#Sat Mar 09 16:55:59 CET 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
@ -0,0 +1,26 @@
|
||||
import java.net.URI
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = URI.create("https://jitpack.io") }
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "IQBall"
|
||||
include(":app")
|