merge remote-tracking branch 'origin/master' into home

home
d_yanis 1 year ago committed by maxime
commit 7aab2a0d08

@ -67,12 +67,18 @@ dependencies {
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.retrofit) implementation(libs.retrofit)
implementation(libs.converter.gson)
implementation(libs.retrofit2.kotlinx.serialization.converter) implementation(libs.retrofit2.kotlinx.serialization.converter)
implementation(libs.kotlinx.datetime) 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.retrofit.adapters.arrow)
implementation(libs.arrow.core) implementation(libs.arrow.core)
implementation(libs.androidx.navigation.compose)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

@ -0,0 +1,62 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1280.000000pt" height="1276.000000pt" viewBox="0 0 1280.000000 1276.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,1276.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M6085 12754 c-1124 -66 -2124 -378 -3055 -952 -113 -70 -150 -98
-115 -88 50 14 350 37 555 43 925 24 1755 -198 2573 -691 95 -56 321 -202 502
-322 511 -340 600 -373 753 -280 281 170 481 457 559 800 24 105 24 393 -1
506 -59 277 -202 557 -417 813 -45 54 -91 102 -103 106 -19 8 -160 25 -411 51
-99 11 -724 21 -840 14z"/>
<path d="M7630 12635 c0 -2 28 -48 63 -102 210 -324 335 -626 378 -913 16
-112 14 -310 -6 -424 -56 -328 -245 -651 -591 -1014 -79 -83 -105 -117 -98
-127 11 -17 953 -895 959 -895 3 0 136 105 297 233 1085 863 1485 1140 2065
1431 94 47 173 91 175 98 7 20 -232 240 -467 429 -766 616 -1654 1043 -2622
1259 -135 31 -153 33 -153 25z"/>
<path d="M3055 11615 c-127 -13 -262 -36 -359 -61 -69 -18 -157 -81 -397 -280
-984 -823 -1695 -1909 -2049 -3135 -84 -291 -140 -550 -185 -854 -19 -130 -29
-231 -23 -225 3 3 16 34 28 70 36 103 124 267 195 363 386 518 1151 901 2475
1238 291 74 553 134 910 209 107 23 236 50 285 60 50 11 158 33 240 50 83 17
191 39 240 50 50 10 133 28 185 39 1079 228 1599 392 1915 604 96 64 205 181
244 262 28 57 35 82 35 135 1 62 -2 70 -44 135 -142 218 -526 495 -1019 737
-920 449 -1941 680 -2676 603z"/>
<path d="M10940 10674 c-63 -41 -176 -114 -250 -162 -299 -193 -798 -547
-1215 -861 -423 -318 -899 -711 -893 -736 2 -6 189 -246 417 -532 l414 -520
171 82 c528 255 975 375 1398 375 574 0 1060 -239 1464 -720 85 -101 219 -286
264 -365 18 -31 34 -54 36 -52 11 11 -69 456 -120 671 -235 976 -692 1875
-1347 2646 -54 63 -126 145 -161 182 l-63 66 -115 -74z"/>
<path d="M7125 9833 c-70 -62 -231 -181 -327 -242 -491 -308 -1120 -531 -2106
-746 -90 -20 -452 -94 -804 -165 -1128 -228 -1491 -317 -1978 -485 -936 -323
-1527 -761 -1860 -1378 l-42 -78 -5 -252 c-31 -1503 472 -2974 1415 -4137 47
-58 87 -107 88 -108 8 -11 50 35 210 233 1067 1318 2042 2408 3174 3549 953
960 1908 1846 2933 2721 135 116 246 215 247 220 0 10 -890 905 -900 905 -3
-1 -23 -17 -45 -37z"/>
<path d="M8255 8618 c-886 -668 -1534 -1216 -2370 -2004 -1028 -969 -2125
-2136 -3299 -3509 -132 -154 -265 -309 -296 -345 -142 -164 -592 -703 -596
-715 -7 -16 300 -326 466 -471 678 -593 1425 -1020 2260 -1294 231 -75 490
-143 730 -190 207 -40 413 -72 560 -85 62 -6 69 -4 155 38 192 94 346 209 515
383 351 363 601 854 839 1649 58 192 158 587 211 835 40 185 114 541 190 920
98 486 168 825 200 965 5 22 16 69 24 105 8 36 36 151 61 255 40 164 59 235
115 435 52 187 169 532 242 715 232 582 562 1062 911 1325 25 19 45 38 44 42
-2 8 -838 1039 -840 1037 -1 0 -56 -41 -122 -91z"/>
<path d="M10570 8093 c-358 -54 -579 -149 -741 -319 -84 -89 -93 -106 -86
-168 10 -100 65 -199 397 -721 368 -578 508 -818 691 -1185 379 -760 567
-1413 611 -2132 24 -392 -30 -863 -142 -1243 -16 -54 -28 -100 -27 -102 8 -8
297 369 419 547 722 1055 1109 2313 1108 3600 0 294 -15 490 -40 547 -30 68
-127 228 -194 318 -240 326 -590 581 -999 729 -271 98 -498 137 -791 135 -100
-1 -192 -3 -206 -6z"/>
<path d="M9265 7317 c-292 -97 -541 -403 -744 -914 -174 -439 -312 -964 -521
-1975 -16 -82 -44 -214 -60 -295 -17 -82 -39 -188 -50 -238 -11 -49 -33 -157
-50 -240 -389 -1893 -798 -2915 -1390 -3478 -57 -54 -123 -112 -147 -130 l-45
-32 203 -3 c568 -9 1247 95 1844 283 928 292 1741 765 2451 1423 179 166 216
207 253 281 96 190 163 413 202 676 20 128 23 192 23 430 0 344 -22 560 -95
927 -191 959 -696 2109 -1249 2843 -175 231 -351 410 -428 433 -64 19 -153 23
-197 9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@ -0,0 +1,135 @@
<svg width="567" height="269" viewBox="0 0 567 269" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="73" y1="24" x2="495" y2="24" stroke="black" stroke-width="2"/>
<line x1="494" y1="23" x2="494" y2="247" stroke="black" stroke-width="2"/>
<line x1="495" y1="248" x2="73" y2="248" stroke="black" stroke-width="2"/>
<line x1="72" y1="249" x2="72" y2="23" stroke="black" stroke-width="2"/>
<line x1="283.5" y1="23" x2="283.5" y2="247" stroke="black"/>
<g filter="url(#filter0_i_3_2)">
<circle cx="283.5" cy="135.5" r="27" stroke="black"/>
</g>
<line x1="73" y1="99.5" x2="158" y2="99.5" stroke="black"/>
<line x1="73" y1="100.5" x2="158" y2="100.5" stroke="black"/>
<path d="M158.5 172V100" stroke="black"/>
<path d="M158 99.5H159" stroke="black"/>
<path d="M158 172.5H159" stroke="black"/>
<line x1="158" y1="172.5" x2="73" y2="172.5" stroke="black"/>
<line x1="158" y1="171.5" x2="73" y2="171.5" stroke="black"/>
<line x1="73" y1="37.5" x2="139" y2="37.5" stroke="black"/>
<line x1="73" y1="233.5" x2="139" y2="233.5" stroke="black"/>
<g filter="url(#filter1_i_3_2)">
<path d="M158.5 163C161.98 163 165.426 162.315 168.641 160.983C171.856 159.651 174.778 157.699 177.238 155.238C179.699 152.778 181.651 149.856 182.983 146.641C184.315 143.426 185 139.98 185 136.5C185 133.02 184.315 129.574 182.983 126.359C181.651 123.144 179.699 120.222 177.238 117.762C174.778 115.301 171.856 113.349 168.641 112.017C165.426 110.685 161.98 110 158.5 110L158.5 136.5L158.5 163Z" stroke="black"/>
</g>
<g filter="url(#filter2_i_3_2)">
<path d="M158.5 110C155.02 110 151.574 110.685 148.359 112.017C145.144 113.349 142.222 115.301 139.762 117.762C137.301 120.222 135.349 123.144 134.017 126.359C132.685 129.574 132 133.02 132 136.5C132 139.98 132.685 143.426 134.017 146.641C135.349 149.856 137.301 152.778 139.762 155.238C142.222 157.699 145.144 159.651 148.359 160.983C151.574 162.315 155.02 163 158.5 163" stroke="black" stroke-dasharray="4 4"/>
</g>
<line x1="135.5" y1="177" x2="135.5" y2="172" stroke="black"/>
<line x1="123.5" y1="177" x2="123.5" y2="172" stroke="black"/>
<line x1="111.5" y1="177" x2="111.5" y2="172" stroke="black"/>
<line x1="99.5" y1="177" x2="99.5" y2="172" stroke="black"/>
<line x1="135.5" y1="100" x2="135.5" y2="95" stroke="black"/>
<line x1="123.5" y1="100" x2="123.5" y2="95" stroke="black"/>
<line x1="111.5" y1="100" x2="111.5" y2="95" stroke="black"/>
<line x1="99.5" y1="100" x2="99.5" y2="95" stroke="black"/>
<path d="M140.212 233.607C159.054 225.612 175.149 211.967 186.427 194.431C197.705 176.895 203.649 156.271 203.497 135.213C203.345 114.155 197.104 93.6242 185.574 76.2645C174.045 58.9047 157.755 45.5096 138.799 37.8066" stroke="black"/>
<path d="M140 233.5H141" stroke="black"/>
<path d="M139 233.5H140" stroke="black"/>
<path d="M90.5 118.5C95.0041 118.5 99.3266 120.34 102.516 123.621C105.706 126.901 107.5 131.354 107.5 136C107.5 140.646 105.706 145.099 102.516 148.379C99.3266 151.66 95.0041 153.5 90.5 153.5" stroke="black"/>
<circle cx="87.5" cy="136.5" r="3" stroke="black"/>
<line x1="83.5" y1="149" x2="83.5" y2="123" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(-1 0 0 1 494 100)" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(-1 0 0 1 494 101)" stroke="black"/>
<path d="M408.5 172V100" stroke="black"/>
<path d="M409 99.5H408" stroke="black"/>
<path d="M409 172.5H408" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(1 0 0 -1 409 172)" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(1 0 0 -1 409 171)" stroke="black"/>
<line y1="-0.5" x2="66" y2="-0.5" transform="matrix(-1 0 0 1 494 38)" stroke="black"/>
<line y1="-0.5" x2="66" y2="-0.5" transform="matrix(-1 0 0 1 494 234)" stroke="black"/>
<g filter="url(#filter3_i_3_2)">
<path d="M408.5 163C405.02 163 401.574 162.315 398.359 160.983C395.144 159.651 392.222 157.699 389.762 155.238C387.301 152.778 385.349 149.856 384.017 146.641C382.685 143.426 382 139.98 382 136.5C382 133.02 382.685 129.574 384.017 126.359C385.349 123.144 387.301 120.222 389.762 117.762C392.222 115.301 395.144 113.349 398.359 112.017C401.574 110.685 405.02 110 408.5 110L408.5 136.5L408.5 163Z" stroke="black"/>
</g>
<g filter="url(#filter4_i_3_2)">
<path d="M408.5 110C411.98 110 415.426 110.685 418.641 112.017C421.856 113.349 424.778 115.301 427.238 117.762C429.699 120.222 431.651 123.144 432.983 126.359C434.315 129.574 435 133.02 435 136.5C435 139.98 434.315 143.426 432.983 146.641C431.651 149.856 429.699 152.778 427.238 155.238C424.778 157.699 421.856 159.651 418.641 160.983C415.426 162.315 411.98 163 408.5 163" stroke="black" stroke-dasharray="4 4"/>
</g>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 431 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 443 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 455 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 467 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 431 100)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 443 100)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 455 100)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 467 100)" stroke="black"/>
<path d="M426.788 233.607C407.946 225.612 391.851 211.967 380.573 194.431C369.295 176.895 363.351 156.271 363.503 135.213C363.655 114.155 369.896 93.6242 381.426 76.2645C392.955 58.9047 409.245 45.5096 428.201 37.8066" stroke="black"/>
<path d="M427 233.5H426" stroke="black"/>
<path d="M428 233.5H427" stroke="black"/>
<g filter="url(#filter5_i_3_2)">
<path d="M476.5 118.5C471.996 118.5 467.673 120.34 464.484 123.621C461.294 126.901 459.5 131.354 459.5 136C459.5 140.646 461.294 145.099 464.484 148.379C467.673 151.66 471.996 153.5 476.5 153.5" stroke="black"/>
</g>
<circle cx="3.5" cy="3.5" r="3" transform="matrix(-1 0 0 1 483 133)" stroke="black"/>
<line y1="-0.5" x2="26" y2="-0.5" transform="matrix(0 -1 -1 0 483 149)" stroke="black"/>
<path d="M138 37.5H139" stroke="black"/>
<path d="M139.225 38.1519C139.221 38.1187 139.099 38.1364 139.073 38.135C138.98 38.1302 138.889 38.0674 138.794 38.0674C138.755 38.0674 138.672 38.0388 138.633 38.0252C138.581 38.0065 138.52 37.9719 138.465 37.9661C138.4 37.9593 138.366 37.9109 138.312 37.8854C138.275 37.8677 138.202 37.8366 138.177 37.8056C138.161 37.7847 138.063 37.7566 138.033 37.7371C137.962 37.6922 137.879 37.6309 137.806 37.5944" stroke="black" stroke-width="0.5"/>
<path d="M139.926 37.9883C139.906 37.9888 139.887 37.9922 139.867 37.9922C139.838 37.9922 139.812 37.978 139.787 37.9622C139.682 37.8946 139.599 37.7958 139.504 37.7151C139.388 37.6172 139.275 37.5101 139.135 37.4512C139.051 37.4159 138.965 37.3877 138.879 37.3594C138.85 37.3498 138.785 37.3113 138.754 37.332" stroke="black" stroke-width="0.5"/>
<defs>
<filter id="filter0_i_3_2" x="256" y="108" width="55" height="59" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
</filter>
<filter id="filter1_i_3_2" x="158" y="109.5" width="27.5" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
</filter>
<filter id="filter2_i_3_2" x="131.5" y="109.5" width="27" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
</filter>
<filter id="filter3_i_3_2" x="381.5" y="109.5" width="27.5" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
</filter>
<filter id="filter4_i_3_2" x="408.5" y="109.5" width="27" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
</filter>
<filter id="filter5_i_3_2" x="459" y="118" width="17.5" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,73 @@
<svg width="269" height="309" viewBox="0 0 269 309" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="24" y1="236" x2="24" y2="26" stroke="black" stroke-width="2"/>
<line x1="248" y1="25" x2="248" y2="236" stroke="black" stroke-width="2"/>
<line x1="249" y1="237" x2="23" y2="237" stroke="black" stroke-width="2"/>
<line x1="23" y1="25.5" x2="247" y2="25.5" stroke="black"/>
<g filter="url(#filter0_i_3_4)">
<path d="M108.5 26C108.5 33.0313 111.347 39.7727 116.411 44.7417C121.476 49.7103 128.342 52.5 135.5 52.5C142.658 52.5 149.524 49.7103 154.588 44.7417C159.653 39.7727 162.5 33.0313 162.5 26" stroke="black"/>
</g>
<line x1="99.5" y1="236" x2="99.5" y2="151" stroke="black"/>
<line x1="100.5" y1="236" x2="100.5" y2="151" stroke="black"/>
<path d="M172 150.5H100" stroke="black"/>
<path d="M99.5 151V150" stroke="black"/>
<path d="M172.5 151V150" stroke="black"/>
<line x1="172.5" y1="151" x2="172.5" y2="236" stroke="black"/>
<line x1="171.5" y1="151" x2="171.5" y2="236" stroke="black"/>
<line x1="37.5" y1="236" x2="37.5" y2="170" stroke="black"/>
<line x1="233.5" y1="236" x2="233.5" y2="170" stroke="black"/>
<g filter="url(#filter1_i_3_4)">
<path d="M163 150.5C163 147.02 162.315 143.574 160.983 140.359C159.651 137.144 157.699 134.222 155.238 131.762C152.778 129.301 149.856 127.349 146.641 126.017C143.426 124.685 139.98 124 136.5 124C133.02 124 129.574 124.685 126.359 126.017C123.144 127.349 120.222 129.301 117.762 131.762C115.301 134.222 113.349 137.144 112.017 140.359C110.685 143.574 110 147.02 110 150.5L136.5 150.5L163 150.5Z" stroke="black"/>
</g>
<g filter="url(#filter2_i_3_4)">
<path d="M110 150.5C110 153.98 110.685 157.426 112.017 160.641C113.349 163.856 115.301 166.778 117.762 169.238C120.222 171.699 123.144 173.651 126.359 174.983C129.574 176.315 133.02 177 136.5 177C139.98 177 143.426 176.315 146.641 174.983C149.856 173.651 152.778 171.699 155.238 169.238C157.699 166.778 159.651 163.856 160.983 160.641C162.315 157.426 163 153.98 163 150.5" stroke="black" stroke-dasharray="4 4"/>
</g>
<line x1="177" y1="173.5" x2="172" y2="173.5" stroke="black"/>
<line x1="177" y1="185.5" x2="172" y2="185.5" stroke="black"/>
<line x1="177" y1="197.5" x2="172" y2="197.5" stroke="black"/>
<line x1="177" y1="209.5" x2="172" y2="209.5" stroke="black"/>
<line x1="100" y1="173.5" x2="95" y2="173.5" stroke="black"/>
<line x1="100" y1="185.5" x2="95" y2="185.5" stroke="black"/>
<line x1="100" y1="197.5" x2="95" y2="197.5" stroke="black"/>
<line x1="100" y1="209.5" x2="95" y2="209.5" stroke="black"/>
<path d="M233.607 168.788C225.612 149.946 211.967 133.851 194.431 122.573C176.895 111.295 156.271 105.351 135.213 105.503C114.155 105.655 93.6242 111.896 76.2645 123.426C58.9047 134.955 45.5096 151.245 37.8066 170.201" stroke="black"/>
<path d="M233.5 169V168" stroke="black"/>
<path d="M233.5 170V169" stroke="black"/>
<path d="M118.5 218.5C118.5 213.996 120.34 209.673 123.621 206.484C126.901 203.294 131.354 201.5 136 201.5C140.646 201.5 145.099 203.294 148.379 206.484C151.66 209.673 153.5 213.996 153.5 218.5" stroke="black"/>
<circle cx="136.5" cy="221.5" r="3" transform="rotate(-90 136.5 221.5)" stroke="black"/>
<line x1="149" y1="225.5" x2="123" y2="225.5" stroke="black"/>
<path d="M37.5 171V170" stroke="black"/>
<path d="M38.1519 169.775C38.1187 169.779 38.1364 169.901 38.135 169.927C38.1302 170.02 38.0674 170.111 38.0674 170.206C38.0674 170.245 38.0388 170.328 38.0252 170.367C38.0065 170.419 37.9719 170.48 37.9661 170.535C37.9593 170.6 37.9109 170.634 37.8854 170.688C37.8677 170.725 37.8366 170.798 37.8056 170.823C37.7847 170.839 37.7566 170.937 37.7371 170.967C37.6922 171.038 37.6309 171.121 37.5944 171.194" stroke="black" stroke-width="0.5"/>
<path d="M37.9883 169.074C37.9888 169.094 37.9922 169.113 37.9922 169.133C37.9922 169.162 37.978 169.188 37.9622 169.213C37.8946 169.318 37.7958 169.401 37.7151 169.496C37.6172 169.612 37.5101 169.725 37.4512 169.865C37.4159 169.949 37.3877 170.035 37.3594 170.121C37.3498 170.15 37.3113 170.215 37.332 170.246" stroke="black" stroke-width="0.5"/>
<defs>
<filter id="filter0_i_3_4" x="108" y="26" width="55" height="31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_4"/>
</filter>
<filter id="filter1_i_3_4" x="109.5" y="123.5" width="54" height="31.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_4"/>
</filter>
<filter id="filter2_i_3_4" x="109.5" y="150.5" width="54" height="31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_4"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

@ -7,31 +7,76 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.compose.ui.Modifier
import com.google.gson.Gson
import com.iqball.app.api.EitherBodyConverter import androidx.navigation.compose.NavHost
import com.iqball.app.api.EitherCallAdapterFactory import androidx.navigation.compose.composable
import com.iqball.app.api.service.AuthService import androidx.navigation.compose.rememberNavController
import com.iqball.app.api.service.IQBallService 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.HomePage
import com.iqball.app.page.LoginPage
import com.iqball.app.page.RegisterPage 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.iqball.app.ui.theme.IQBallTheme
import kotlinx.coroutines.runBlocking import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create import retrofit2.create
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val gson = Gson()
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() val retrofit = Retrofit.Builder()
.addConverterFactory(EitherBodyConverter.create()) .addConverterFactory(EitherBodyConverter.create())
.addConverterFactory(GsonConverterFactory.create(gson)) .addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(EitherCallAdapterFactory.create(gson)) .addCallAdapterFactory(EitherCallAdapterFactory.create())
.baseUrl("https://iqball.maxou.dev/api/dotnet-master/") .baseUrl("https://iqball.maxou.dev/api/dotnet-master/")
.client( .client(
OkHttpClient.Builder() OkHttpClient.Builder()
@ -49,9 +94,47 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
HomePage(service) 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,11 @@
package com.iqball.app.model
import com.iqball.app.model.tactic.CourtType
import java.time.LocalDateTime
data class TacticInfo(
val id: Int,
val name: String,
val type: CourtType,
val creationDate: LocalDateTime
)

@ -0,0 +1,24 @@
package com.iqball.app.model.tactic
import arrow.core.Either
import arrow.core.NonEmptyList
import com.iqball.app.geo.Vector
enum class ActionType {
Screen,
Dribble,
Move,
Shoot
}
data class Segment(
val next: Either<ComponentId, Vector>,
val controlPoint: Vector?
)
data class Action(
val type: ActionType,
val target: Either<ComponentId, Vector>,
val segments: List<Segment>
)

@ -0,0 +1,13 @@
package com.iqball.app.model.tactic
enum class BallState {
None,
HoldsOrigin,
HoldsByPass,
Passed,
PassedOrigin;
fun hasBall() = this != None
}

@ -0,0 +1,6 @@
package com.iqball.app.model.tactic
enum class CourtType {
Plain,
Half
}

@ -0,0 +1,3 @@
package com.iqball.app.model.tactic
data class MovementPath(val items: List<ComponentId>)

@ -0,0 +1,13 @@
package com.iqball.app.model.tactic
import com.iqball.app.geo.Vector
data class PlayerInfo(
val team: PlayerTeam,
val role: String,
val isPhantom: Boolean,
val pos: Vector,
val id: ComponentId,
val actions: List<Action>,
val ballState: BallState
)

@ -0,0 +1,6 @@
package com.iqball.app.model.tactic
enum class PlayerTeam {
Allies,
Opponents
}

@ -0,0 +1,9 @@
package com.iqball.app.model.tactic
import com.iqball.app.geo.Pos
sealed interface Positioning
data class RelativePositioning(val attach: ComponentId) : Positioning
data class FixedPosition(val x: Float, val y: Float) : Positioning {
fun toPos() = Pos(x, y)
}

@ -0,0 +1,42 @@
package com.iqball.app.model.tactic
typealias ComponentId = String
sealed interface StepComponent {
val id: ComponentId
val actions: List<Action>
}
sealed interface PositionableComponent<P : Positioning> {
val pos: P
}
sealed interface PlayerLike : PositionableComponent<Positioning>, StepComponent {
val ballState: BallState
}
data class PlayerComponent(
val path: MovementPath?,
val team: PlayerTeam,
val role: String,
override val ballState: BallState,
override val pos: FixedPosition,
override val id: ComponentId,
override val actions: List<Action>,
) : PlayerLike, StepComponent
data class PhantomComponent(
val attachedTo: ComponentId?,
val originPlayerId: ComponentId,
override val ballState: BallState,
override val pos: Positioning,
override val id: ComponentId,
override val actions: List<Action>
) : PlayerLike, StepComponent
data class BallComponent(
override val id: ComponentId,
override val actions: List<Action>,
override val pos: FixedPosition
) : StepComponent, PositionableComponent<FixedPosition>

@ -0,0 +1,15 @@
package com.iqball.app.model.tactic
import java.lang.RuntimeException
data class StepContent(val components: List<StepComponent>) {
inline fun <reified C: StepComponent> findComponent(id: String): C? {
val value = components.find { it.id == id } ?: return null
if (!C::class.java.isAssignableFrom(value.javaClass))
return null
return value as C
}
}
class MalformedStepContentException(msg: String, cause: Throwable? = null): RuntimeException(msg, cause)

@ -0,0 +1,17 @@
package com.iqball.app.model.tactic
data class StepNodeInfo(val id: Int, val children: List<StepNodeInfo>)
fun getParent(root: StepNodeInfo, child: Int): StepNodeInfo? {
for (children in root.children) {
if (children.id == child) {
return root
}
val result = getParent(children, child)
if (result != null)
return result
}
return null
}

@ -1,4 +1,4 @@
package com.iqball.app.api package com.iqball.app.net
import arrow.core.Either import arrow.core.Either
import okhttp3.ResponseBody import okhttp3.ResponseBody

@ -1,16 +1,16 @@
package com.iqball.app.api package com.iqball.app.net
import arrow.core.Either import arrow.core.Either
import com.google.gson.Gson
import okhttp3.Request import okhttp3.Request
import okio.Timeout import okio.Timeout
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
class EitherCall<L, R>( class EitherCall<L, R>(
private val gson: Gson, private val retrofit: Retrofit,
private val eitherType: ParameterizedType, private val eitherType: ParameterizedType,
private val delegate: Call<Any> private val delegate: Call<Any>
) : Call<Either<L, R>> { ) : Call<Either<L, R>> {
@ -21,8 +21,9 @@ class EitherCall<L, R>(
Either.Right(response.body()!! as R) Either.Right(response.body()!! as R)
} else { } else {
val leftType = eitherType.actualTypeArguments[0] val leftType = eitherType.actualTypeArguments[0]
val parsed = gson.fromJson<L>(response.errorBody()!!.charStream(), leftType) val converter = retrofit.nextResponseBodyConverter<L>(null, leftType, arrayOf())
Either.Left(parsed) val result = converter.convert(response.errorBody()!!)!!
Either.Left(result)
} }
callback.onResponse(this@EitherCall, Response.success(result)) callback.onResponse(this@EitherCall, Response.success(result))
} }
@ -34,7 +35,7 @@ class EitherCall<L, R>(
}) })
} }
override fun clone(): Call<Either<L, R>> = EitherCall(gson, eitherType, delegate.clone()) override fun clone(): Call<Either<L, R>> = EitherCall(retrofit, eitherType, delegate.clone())
override fun execute(): Response<Either<L, R>> { override fun execute(): Response<Either<L, R>> {
throw UnsupportedOperationException() throw UnsupportedOperationException()

@ -1,17 +1,13 @@
package com.iqball.app.api package com.iqball.app.net
import android.os.Build
import androidx.annotation.RequiresApi
import arrow.core.Either import arrow.core.Either
import com.google.gson.Gson
import com.skydoves.retrofit.adapters.arrow.EitherCallAdapterFactory
import retrofit2.Call import retrofit2.Call
import retrofit2.CallAdapter import retrofit2.CallAdapter
import retrofit2.Retrofit import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
class EitherCallAdapterFactory(private val gson: Gson) : CallAdapter.Factory() { class EitherCallAdapterFactory : CallAdapter.Factory() {
override fun get( override fun get(
returnType: Type, returnType: Type,
annotations: Array<out Annotation>, annotations: Array<out Annotation>,
@ -31,12 +27,12 @@ class EitherCallAdapterFactory(private val gson: Gson) : CallAdapter.Factory() {
override fun responseType(): Type = returnType override fun responseType(): Type = returnType
override fun adapt(call: Call<Any>): EitherCall<Any, Any> { override fun adapt(call: Call<Any>): EitherCall<Any, Any> {
return EitherCall(gson, eitherType, call) return EitherCall(retrofit, eitherType, call)
} }
} }
} }
companion object { companion object {
fun create(gson: Gson) = EitherCallAdapterFactory(gson) fun create() = EitherCallAdapterFactory()
} }
} }

@ -1,17 +1,13 @@
package com.iqball.app.api.service package com.iqball.app.net.service
import arrow.core.Either
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
interface AuthService { interface AuthService {
@Serializable @Serializable
data class AuthResponse(val token: String, val expirationDate: String) data class AuthResponse(val token: String, val expirationDate: Long)
@Serializable @Serializable
data class RegisterRequest(val username: String, val email: String, val password: String) data class RegisterRequest(val username: String, val email: String, val password: String)

@ -1,9 +1,8 @@
package com.iqball.app.api.service package com.iqball.app.net.service
import arrow.core.Either import arrow.core.Either
import retrofit2.Call
typealias ErrorResponseResult = Map<String, Array<String>> typealias ErrorResponseResult = Map<String, Array<String>>
typealias APIResult<R> = Either<ErrorResponseResult, R> typealias APIResult<R> = Either<ErrorResponseResult, R>
interface IQBallService : AuthService, UserService interface IQBallService : AuthService, UserService, TacticService

@ -0,0 +1,44 @@
package com.iqball.app.net.service
import com.iqball.app.model.tactic.StepContent
import com.iqball.app.model.tactic.StepNodeInfo
import com.iqball.app.session.Token
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
interface TacticService {
data class GetTacticInfoResponse(
val id: Int,
val name: String,
val courtType: String,
val creationDate: Long
)
@GET("tactics/{tacticId}")
suspend fun getTacticInfo(
@Header("Authorization") auth: Token,
@Path("tacticId") tacticId: Int
): APIResult<GetTacticInfoResponse>
data class GetTacticStepsTreeResponse(
val root: StepNodeInfo
)
@GET("tactics/{tacticId}/tree")
suspend fun getTacticStepsTree(
@Header("Authorization") auth: Token,
@Path("tacticId") tacticId: Int
): APIResult<GetTacticStepsTreeResponse>
@GET("tactics/{tacticId}/steps/{stepId}")
suspend fun getTacticStepContent(
@Header("Authorization") auth: Token,
@Path("tacticId") tacticId: Int,
@Path("stepId") stepId: Int
): APIResult<StepContent>
}

@ -1,4 +1,4 @@
package com.iqball.app.api.service package com.iqball.app.net.service
import com.iqball.app.model.Tactic import com.iqball.app.model.Tactic
import com.iqball.app.model.Team import com.iqball.app.model.Team

@ -1,5 +1,7 @@
package com.iqball.app.page 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.BorderStroke
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -31,14 +33,11 @@ import androidx.compose.material3.TopAppBarDefaults.pinnedScrollBehavior
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.iqball.app.model.Tactic
import com.iqball.app.model.Team
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
@ -49,9 +48,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import arrow.core.Either import arrow.core.Either
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.iqball.app.api.service.AuthService import com.iqball.app.net.service.AuthService
import com.iqball.app.api.service.IQBallService import com.iqball.app.net.service.IQBallService
import com.iqball.app.api.service.UserService import com.iqball.app.net.service.UserService
import com.iqball.app.session.Authentication
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
@ -60,12 +60,12 @@ import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomePage(service: IQBallService) { fun HomePage(service: IQBallService, auth: Authentication) {
val tactics: List<Tactic> val tactics: List<Tactic>
val teams: List<Team> val teams: List<Team>
var invalid = false var invalid = false
val data = getDataFromApi(service) val data = getDataFromApi(service, auth)
if (data == null) { if (data == null) {
tactics = listOf<Tactic>() tactics = listOf<Tactic>()
teams = listOf<Team>() teams = listOf<Team>()
@ -75,7 +75,6 @@ fun HomePage(service: IQBallService) {
tactics = data.tactics tactics = data.tactics
teams = data.teams teams = data.teams
} }
val scrollBehavior = pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@ -102,7 +101,12 @@ fun HomePage(service: IQBallService) {
} }
@Composable @Composable
private fun Body(padding: PaddingValues, tactics: List<Tactic>, teams: List<Team>, invalid: Boolean) { private fun Body(
padding: PaddingValues,
tactics: List<Tactic>,
teams: List<Team>,
invalid: Boolean
) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
@ -110,12 +114,12 @@ private fun Body(padding: PaddingValues, tactics: List<Tactic>, teams: List<Team
val selectedTab = remember { mutableIntStateOf(0) } val selectedTab = remember { mutableIntStateOf(0) }
val tabs = listOf<Pair<String, @Composable () -> Unit>>( val tabs = listOf<Pair<String, @Composable () -> Unit>>(
Pair("Espace personnel") { Pair("Espace personnel") {
ListComponentCard<Tactic>(tactics) { tactic -> ListComponentCard(tactics) { tactic ->
TacticCard(tactic = tactic) TacticCard(tactic = tactic)
} }
}, },
Pair("Mes Equipes") { Pair("Mes équipes") {
ListComponentCard<Team>(teams) { team -> ListComponentCard(teams) { team ->
TeamCard(team = team) TeamCard(team = team)
} }
} }
@ -125,12 +129,13 @@ private fun Body(padding: PaddingValues, tactics: List<Tactic>, teams: List<Team
tabs[selectedTab.intValue].second() tabs[selectedTab.intValue].second()
return return
} }
}
TextCentered( TextCentered(
text = "Erreur : Aucune connexion internet. Veillez activer votre connexion internet puis relancer l'application", text = "Erreur : Aucune connexion internet. Veillez activer votre connexion internet puis relancer l'application",
fontSize = 20.sp fontSize = 20.sp
) )
}
} }
@Composable @Composable
@ -318,15 +323,14 @@ private fun TextCentered(
) )
} }
private fun getDataFromApi(service: IQBallService): UserService.UserDataResponse? { private fun getDataFromApi(
service: IQBallService,
auth: Authentication
): UserService.UserDataResponse? {
var res: UserService.UserDataResponse? = null var res: UserService.UserDataResponse? = null
try { try {
runBlocking { runBlocking {
val result = service.login(AuthService.LoginRequest("yanis@mail.com", "123456")) val data = service.getUserData(auth.token)
when (result) {
is Either.Left -> null
is Either.Right -> {
val data = service.getUserData(result.value.token)
when (data) { when (data) {
is Either.Left -> null is Either.Left -> null
is Either.Right -> { is Either.Right -> {
@ -334,8 +338,6 @@ private fun getDataFromApi(service: IQBallService): UserService.UserDataResponse
} }
} }
} }
}
}
return res return res
} catch (error: Exception) { } catch (error: Exception) {
return res return res

@ -1,9 +1,109 @@
package com.iqball.app.page package com.iqball.app.page
import android.util.Log
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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 @Composable
fun LoginPage() { fun LoginPage(
Text(text = "Login Page") service: AuthService,
onLoginSuccess: (Authentication) -> Unit,
onNavigateToRegister: () -> Unit
) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var errors by remember { mutableStateOf("") }
Surface(
color = Color.White,
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "S'identifier",
fontSize = 28.sp,
color = Color.Black
)
Text(
text = errors,
color = Color.Red,
fontSize = 14.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Black,
unfocusedTextColor = Color.Black
)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Black,
unfocusedTextColor = Color.Black
)
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
runBlocking {
Log.i("ZIZI", "On click wesh")
when (val response = service.login(AuthService.LoginRequest(email, password))) {
is Either.Left -> {
errors = response.value.toList()
.flatMap { entry -> entry.second.map { "${entry.first} : ${it}" } }
.joinToString("\n")
}
is Either.Right -> onLoginSuccess(
Authentication(
response.value.token,
response.value.expirationDate.toLong()
)
)
}
}
}) {
Text(text = "Se connecter")
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { onNavigateToRegister() }) {
Text(text = "Vous n'avez pas de compte ?")
}
}
}
} }

@ -1,54 +1,130 @@
package com.iqball.app.page package com.iqball.app.page
import android.util.Log import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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 arrow.core.Either
import com.iqball.app.api.service.AuthService import com.iqball.app.net.service.AuthService
import com.iqball.app.api.service.AuthService.RegisterRequest
import com.iqball.app.api.service.IQBallService
import com.iqball.app.session.Authentication import com.iqball.app.session.Authentication
import com.iqball.app.session.MutableSession
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@Composable @Composable
fun RegisterPage(service: IQBallService) { fun RegisterPage(
service: AuthService,
onRegisterSuccess: (Authentication) -> Unit,
onNavigateToLogin: () -> Unit
) {
var username by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var errors by remember { mutableStateOf("") }
Surface(
color = Color.White,
modifier = Modifier.fillMaxSize()
var text by remember { mutableStateOf("No message !") } ) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "S'enregistrer",
fontSize = 28.sp,
color = Color.Black
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = errors,
color = Color.Red,
fontSize = 14.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Nom d'utilisateur") },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Black,
unfocusedTextColor = Color.Black
)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Mot de passe") },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Black,
unfocusedTextColor = Color.Black
),
visualTransformation = PasswordVisualTransformation()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Black,
unfocusedTextColor = Color.Black
)
)
Button(onClick = {
runBlocking { runBlocking {
val result = service.login(AuthService.LoginRequest("maxime@mail.com", "123456")) when (val response =
service.register(AuthService.RegisterRequest(username, email, password))) {
when (result) {
is Either.Left -> { is Either.Left -> {
println("Error : " + result.value) errors = response.value.toList()
text = result.toString() .flatMap { entry -> entry.second.map { "${entry.first} : ${it}" } }
.joinToString("\n")
} }
is Either.Right -> {
val token = result.value.token
val userDataResponse = service.getUserData(token)
when (userDataResponse) { is Either.Right -> {
is Either.Left -> println("Error User Data : " + userDataResponse.value) onRegisterSuccess(
is Either.Right -> println("Success User Data : " + userDataResponse.value) Authentication(
response.value.token,
response.value.expirationDate
)
)
} }
text = userDataResponse.toString()
} }
} }
}) {
println(result) Text(text = "Créer votre compte")
Log.i("%", result.toString()) }
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { onNavigateToLogin() }) {
Text(text = "Vous avez déjà un compte ?")
}
}
} }
Text(text = text)
} }

@ -0,0 +1,213 @@
package com.iqball.app.page
import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.zIndex
import arrow.core.Either
import com.iqball.app.R
import com.iqball.app.component.BasketCourt
import com.iqball.app.component.BasketCourtStates
import com.iqball.app.component.StepsTree
import com.iqball.app.model.TacticInfo
import com.iqball.app.model.tactic.ComponentId
import com.iqball.app.model.tactic.CourtType
import com.iqball.app.model.tactic.StepContent
import com.iqball.app.model.tactic.StepNodeInfo
import com.iqball.app.model.tactic.getParent
import com.iqball.app.net.service.TacticService
import com.iqball.app.session.Token
import kotlinx.coroutines.runBlocking
import net.engawapg.lib.zoomable.ZoomState
import net.engawapg.lib.zoomable.rememberZoomState
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
private data class VisualizerInitialData(
val info: TacticInfo,
val rootStep: StepNodeInfo,
)
@Composable
fun VisualizerPage(
service: TacticService,
auth: Token,
tacticId: Int,
) {
val dataEither = remember { initializeVisualizer(service, auth, tacticId) }
val showTree = remember { mutableStateOf(true) }
val (info, stepsTree) = when (dataEither) {
// On error return a text to print it to the user
is Either.Left -> return Text(text = dataEither.value)
is Either.Right -> dataEither.value
}
fun getStepContent(step: Int): StepContent = runBlocking {
val result = service.getTacticStepContent(auth, tacticId, step).onLeft {
Log.e(
"received error response from server when retrieving step content: {}",
it.toString()
)
}
when (result) {
is Either.Left -> throw Error("Unexpected error")
is Either.Right -> result.value
}
}
val screenOrientation = LocalConfiguration.current.orientation
var selectedStepId by rememberSaveable { mutableIntStateOf(stepsTree.id) }
val (content, parentContent) = remember(selectedStepId) {
val parentId = getParent(stepsTree, selectedStepId)?.id
Pair(
getStepContent(selectedStepId),
parentId?.let { getStepContent(it) }
)
}
Column {
VisualizerHeader(title = info.name, showTree)
when (screenOrientation) {
Configuration.ORIENTATION_PORTRAIT -> StepsTree(root = stepsTree,
selectedNodeId = selectedStepId,
onNodeSelected = { selectedStepId = it.id })
Configuration.ORIENTATION_LANDSCAPE -> {
val courtArea = remember { mutableStateOf(Rect.Zero) }
val stepOffsets =
remember(selectedStepId) { mutableStateMapOf<ComponentId, Offset>() }
val parentOffsets =
remember(selectedStepId) { mutableStateMapOf<ComponentId, Offset>() }
val courtModifier =
if (showTree.value) Modifier.width(IntrinsicSize.Min) else Modifier.fillMaxWidth()
val courtZoomState = remember { ZoomState() }
Row(modifier = Modifier.background(Color.LightGray)) {
BasketCourt(
content = content,
parentContent,
type = info.type,
modifier = courtModifier,
state = BasketCourtStates(
stepOffsets,
parentOffsets,
courtArea,
courtZoomState
)
)
if (showTree.value) {
StepsTree(
root = stepsTree,
selectedNodeId = selectedStepId,
onNodeSelected = { selectedStepId = it.id }
)
}
}
}
else -> throw Exception("Could not determine device's orientation.")
}
}
}
@Composable
private fun VisualizerHeader(title: String, showTree: MutableState<Boolean>) {
Row(
modifier = Modifier
.fillMaxWidth()
.zIndex(10000F)
.background(Color.White),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.Black
)
}
Text(text = title, color = Color.Black)
IconButton(onClick = { showTree.value = !showTree.value }) {
Icon(
painter = painterResource(id = R.drawable.tree_icon),
contentDescription = "toggle show tree"
)
}
}
}
private fun initializeVisualizer(
service: TacticService, auth: Token, tacticId: Int
): Either<String, VisualizerInitialData> {
val (tacticInfo, tacticTree) = runBlocking {
val tacticInfo = service.getTacticInfo(auth, tacticId).map {
TacticInfo(
id = it.id, name = it.name, type = CourtType.valueOf(
it.courtType.lowercase().replaceFirstChar(Char::uppercaseChar)
), creationDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(it.creationDate), ZoneId.systemDefault()
)
)
}.onLeft {
Log.e(
"received error response from server when retrieving tacticInfo : {}", it.toString()
)
}
val tacticTree = service.getTacticStepsTree(auth, tacticId).map { it.root }.onLeft {
Log.e(
"received error response from server when retrieving tactic steps tree: {}",
it.toString()
)
}
Pair(tacticInfo.getOrNull(), tacticTree.getOrNull())
}
if (tacticInfo == null || tacticTree == null) {
return Either.Left("Unable to retrieve tactic information")
}
return Either.Right(VisualizerInitialData(tacticInfo, tacticTree))
}

@ -0,0 +1,97 @@
package com.iqball.app.serialization
import arrow.core.Either
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.rawType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
private class EitherTypeAdapter(
val leftType: Class<*>,
val rightType: Class<*>,
private val moshi: Moshi
) : JsonAdapter<Either<*, *>>() {
private val leftJsonType = leftType.getJsonPrimitive()
private val rightJsonType = rightType.getJsonPrimitive()
override fun fromJson(reader: JsonReader): Either<*, *>? {
val valueJsonType = when (val token = reader.peek()) {
JsonReader.Token.BEGIN_ARRAY -> JsonPrimitiveType.Array
JsonReader.Token.BEGIN_OBJECT -> JsonPrimitiveType.Object
JsonReader.Token.STRING -> JsonPrimitiveType.String
JsonReader.Token.NUMBER -> JsonPrimitiveType.Number
JsonReader.Token.BOOLEAN -> JsonPrimitiveType.Boolean
JsonReader.Token.NULL -> {
reader.nextNull<Any>()
return null
}
else -> throw JsonDataException("unexpected token : $token")
}
if (valueJsonType == leftJsonType) {
val value = moshi.adapter<Any>(leftType).fromJson(reader)
return Either.Left(value)
}
if (valueJsonType == rightJsonType) {
val value = moshi.adapter<Any>(rightType).fromJson(reader)
return Either.Right(value)
}
throw ClassCastException("Cannot cast a json value of type " + valueJsonType + " as either " + leftType.name.lowercase() + " or " + rightType.name.lowercase())
}
override fun toJson(writer: JsonWriter, value: Either<*, *>?) {
when (value) {
is Either.Left -> writer.jsonValue(value.value)
is Either.Right -> writer.jsonValue(value.value)
null -> writer.nullValue()
}
}
}
object EitherTypeAdapterFactory : JsonAdapter.Factory {
override fun create(
type: Type,
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
if (type !is ParameterizedType)
return null
if (type.rawType != Either::class.java)
return null
val leftType = type.actualTypeArguments[0].rawType
val rightType = type.actualTypeArguments[1].rawType
if (leftType.getJsonPrimitive() == rightType.getJsonPrimitive()) {
throw UnsupportedOperationException("Cannot handle Either types with both sides being object, array, string or number. Provided type is : $type")
}
return EitherTypeAdapter(leftType, rightType, moshi)
}
}
private enum class JsonPrimitiveType {
Array,
Object,
String,
Number,
Boolean
}
private fun Class<*>.getJsonPrimitive(): JsonPrimitiveType {
if (isPrimitive)
return if (java.lang.Boolean.TYPE == this) JsonPrimitiveType.Boolean else JsonPrimitiveType.Number
if (isArray)
return JsonPrimitiveType.Array
if (this == String::class.java)
return JsonPrimitiveType.String
return JsonPrimitiveType.Object
}

@ -0,0 +1,81 @@
package com.iqball.app.serialization
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.rawType
import java.lang.reflect.Type
import java.util.NoSuchElementException
class EnumTypeAdapter<E : Enum<E>>(
values: Map<String, E>,
private val bindNames: Boolean,
private val ignoreCase: Boolean,
private val fallback: E?,
private val clazz: Class<E>
) : JsonAdapter<E>() {
private val values = if (ignoreCase) values.mapKeys { it.key.lowercase() } else values
override fun fromJson(reader: JsonReader): E {
val value = reader.nextString()
val key = if (ignoreCase) value.lowercase() else value
var result = values[key]
if (result == null && bindNames) {
result = clazz.enumConstants?.find { it.name.lowercase() == key }
}
return result
?: fallback
?: throw NoSuchElementException("No enum variant matched given values bindings, and no fallback was provided (value = $value, enum type = $clazz)")
}
override fun toJson(writer: JsonWriter, value: E?) {
throw UnsupportedOperationException()
}
}
class EnumTypeAdapterFactory<E : Enum<E>>(
private val values: Map<String, E>,
private val bindNames: Boolean,
private val ignoreCase: Boolean,
private val fallback: E?,
private val clazz: Class<E>
) : JsonAdapter.Factory {
override fun create(
type: Type,
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
if (type.rawType != clazz)
return null
return EnumTypeAdapter(values, bindNames, ignoreCase, fallback, clazz)
}
companion object {
class Builder<E : Enum<E>>(val values: MutableMap<String, E> = HashMap()) {
infix fun String.means(e: E) {
values[this] = e
}
}
inline fun <reified E : Enum<E>> create(
ignoreCase: Boolean = false,
bindNames: Boolean = true,
fallback: E? = null,
build: Builder<E>.() -> Unit = {}
): EnumTypeAdapterFactory<E> {
val builder = Builder<E>()
build(builder)
return EnumTypeAdapterFactory(builder.values, bindNames, ignoreCase, fallback, E::class.java)
}
}
}

@ -1,5 +1,4 @@
package com.iqball.app.session package com.iqball.app.session
typealias Token = String
import kotlinx.datetime.LocalDateTime data class Authentication(val token: String, val expirationDate: Long)
data class Authentication(val token: String, val expirationDate: LocalDateTime)

@ -0,0 +1,3 @@
package com.iqball.app.session
data class DataSession(override val auth: Authentication? = null) : Session

@ -1,5 +0,0 @@
package com.iqball.app.session
interface MutableSession : Session {
override var auth: Authentication
}

@ -1,5 +1,5 @@
package com.iqball.app.session package com.iqball.app.session
interface Session { interface Session {
val auth: Authentication val auth: Authentication?
} }

@ -17,4 +17,9 @@ val grey = Color(0xFF282A36)
val back = Color(0xFFf8f8f8) val back = Color(0xFFf8f8f8)
val borderCard = Color(0xFFDADCE0) 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,38 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1280dp"
android:height="1276dp"
android:viewportWidth="1280"
android:viewportHeight="1276">
<path
android:pathData="M608.5,0.6c-112.4,6.6 -212.4,37.8 -305.5,95.2 -11.3,7 -15,9.8 -11.5,8.8 5,-1.4 35,-3.7 55.5,-4.3 92.5,-2.4 175.5,19.8 257.3,69.1 9.5,5.6 32.1,20.2 50.2,32.2 51.1,34 60,37.3 75.3,28 28.1,-17 48.1,-45.7 55.9,-80 2.4,-10.5 2.4,-39.3 -0.1,-50.6 -5.9,-27.7 -20.2,-55.7 -41.7,-81.3 -4.5,-5.4 -9.1,-10.2 -10.3,-10.6 -1.9,-0.8 -16,-2.5 -41.1,-5.1 -9.9,-1.1 -72.4,-2.1 -84,-1.4z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M763,12.5c0,0.2 2.8,4.8 6.3,10.2 21,32.4 33.5,62.6 37.8,91.3 1.6,11.2 1.4,31 -0.6,42.4 -5.6,32.8 -24.5,65.1 -59.1,101.4 -7.9,8.3 -10.5,11.7 -9.8,12.7 1.1,1.7 95.3,89.5 95.9,89.5 0.3,-0 13.6,-10.5 29.7,-23.3 108.5,-86.3 148.5,-114 206.5,-143.1 9.4,-4.7 17.3,-9.1 17.5,-9.8 0.7,-2 -23.2,-24 -46.7,-42.9 -76.6,-61.6 -165.4,-104.3 -262.2,-125.9 -13.5,-3.1 -15.3,-3.3 -15.3,-2.5z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M305.5,114.5c-12.7,1.3 -26.2,3.6 -35.9,6.1 -6.9,1.8 -15.7,8.1 -39.7,28 -98.4,82.3 -169.5,190.9 -204.9,313.5 -8.4,29.1 -14,55 -18.5,85.4 -1.9,13 -2.9,23.1 -2.3,22.5 0.3,-0.3 1.6,-3.4 2.8,-7 3.6,-10.3 12.4,-26.7 19.5,-36.3 38.6,-51.8 115.1,-90.1 247.5,-123.8 29.1,-7.4 55.3,-13.4 91,-20.9 10.7,-2.3 23.6,-5 28.5,-6 5,-1.1 15.8,-3.3 24,-5 8.3,-1.7 19.1,-3.9 24,-5 5,-1 13.3,-2.8 18.5,-3.9 107.9,-22.8 159.9,-39.2 191.5,-60.4 9.6,-6.4 20.5,-18.1 24.4,-26.2 2.8,-5.7 3.5,-8.2 3.5,-13.5 0.1,-6.2 -0.2,-7 -4.4,-13.5 -14.2,-21.8 -52.6,-49.5 -101.9,-73.7 -92,-44.9 -194.1,-68 -267.6,-60.3z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M1094,208.6c-6.3,4.1 -17.6,11.4 -25,16.2 -29.9,19.3 -79.8,54.7 -121.5,86.1 -42.3,31.8 -89.9,71.1 -89.3,73.6 0.2,0.6 18.9,24.6 41.7,53.2l41.4,52 17.1,-8.2c52.8,-25.5 97.5,-37.5 139.8,-37.5 57.4,-0 106,23.9 146.4,72 8.5,10.1 21.9,28.6 26.4,36.5 1.8,3.1 3.4,5.4 3.6,5.2 1.1,-1.1 -6.9,-45.6 -12,-67.1 -23.5,-97.6 -69.2,-187.5 -134.7,-264.6 -5.4,-6.3 -12.6,-14.5 -16.1,-18.2l-6.3,-6.6 -11.5,7.4z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M712.5,292.7c-7,6.2 -23.1,18.1 -32.7,24.2 -49.1,30.8 -112,53.1 -210.6,74.6 -9,2 -45.2,9.4 -80.4,16.5 -112.8,22.8 -149.1,31.7 -197.8,48.5 -93.6,32.3 -152.7,76.1 -186,137.8l-4.2,7.8 -0.5,25.2c-3.1,150.3 47.2,297.4 141.5,413.7 4.7,5.8 8.7,10.7 8.8,10.8 0.8,1.1 5,-3.5 21,-23.3 106.7,-131.8 204.2,-240.8 317.4,-354.9 95.3,-96 190.8,-184.6 293.3,-272.1 13.5,-11.6 24.6,-21.5 24.7,-22 0,-1 -89,-90.5 -90,-90.5 -0.3,0.1 -2.3,1.7 -4.5,3.7z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M825.5,414.2c-88.6,66.8 -153.4,121.6 -237,200.4 -102.8,96.9 -212.5,213.6 -329.9,350.9 -13.2,15.4 -26.5,30.9 -29.6,34.5 -14.2,16.4 -59.2,70.3 -59.6,71.5 -0.7,1.6 30,32.6 46.6,47.1 67.8,59.3 142.5,102 226,129.4 23.1,7.5 49,14.3 73,19 20.7,4 41.3,7.2 56,8.5 6.2,0.6 6.9,0.4 15.5,-3.8 19.2,-9.4 34.6,-20.9 51.5,-38.3 35.1,-36.3 60.1,-85.4 83.9,-164.9 5.8,-19.2 15.8,-58.7 21.1,-83.5 4,-18.5 11.4,-54.1 19,-92 9.8,-48.6 16.8,-82.5 20,-96.5 0.5,-2.2 1.6,-6.9 2.4,-10.5 0.8,-3.6 3.6,-15.1 6.1,-25.5 4,-16.4 5.9,-23.5 11.5,-43.5 5.2,-18.7 16.9,-53.2 24.2,-71.5 23.2,-58.2 56.2,-106.2 91.1,-132.5 2.5,-1.9 4.5,-3.8 4.4,-4.2 -0.2,-0.8 -83.8,-103.9 -84,-103.7 -0.1,-0 -5.6,4.1 -12.2,9.1z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M1057,466.7c-35.8,5.4 -57.9,14.9 -74.1,31.9 -8.4,8.9 -9.3,10.6 -8.6,16.8 1,10 6.5,19.9 39.7,72.1 36.8,57.8 50.8,81.8 69.1,118.5 37.9,76 56.7,141.3 61.1,213.2 2.4,39.2 -3,86.3 -14.2,124.3 -1.6,5.4 -2.8,10 -2.7,10.2 0.8,0.8 29.7,-36.9 41.9,-54.7 72.2,-105.5 110.9,-231.3 110.8,-360 0,-29.4 -1.5,-49 -4,-54.7 -3,-6.8 -12.7,-22.8 -19.4,-31.8 -24,-32.6 -59,-58.1 -99.9,-72.9 -27.1,-9.8 -49.8,-13.7 -79.1,-13.5 -10,0.1 -19.2,0.3 -20.6,0.6z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M926.5,544.3c-29.2,9.7 -54.1,40.3 -74.4,91.4 -17.4,43.9 -31.2,96.4 -52.1,197.5 -1.6,8.2 -4.4,21.4 -6,29.5 -1.7,8.2 -3.9,18.8 -5,23.8 -1.1,4.9 -3.3,15.7 -5,24 -38.9,189.3 -79.8,291.5 -139,347.8 -5.7,5.4 -12.3,11.2 -14.7,13l-4.5,3.2 20.3,0.3c56.8,0.9 124.7,-9.5 184.4,-28.3 92.8,-29.2 174.1,-76.5 245.1,-142.3 17.9,-16.6 21.6,-20.7 25.3,-28.1 9.6,-19 16.3,-41.3 20.2,-67.6 2,-12.8 2.3,-19.2 2.3,-43 0,-34.4 -2.2,-56 -9.5,-92.7 -19.1,-95.9 -69.6,-210.9 -124.9,-284.3 -17.5,-23.1 -35.1,-41 -42.8,-43.3 -6.4,-1.9 -15.3,-2.3 -19.7,-0.9z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
</vector>

@ -0,0 +1,171 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="270"
android:viewportHeight="270">
<path
android:pathData="M24,236L24,26"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M248,25L248,236"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M249,237L23,237"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M23,25.5L247,25.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M108.5,26C108.5,33.03 111.35,39.77 116.41,44.74C121.48,49.71 128.34,52.5 135.5,52.5C142.66,52.5 149.52,49.71 154.59,44.74C159.65,39.77 162.5,33.03 162.5,26"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M99.5,236L99.5,151"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M100.5,236L100.5,151"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M172,150.5H100"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M99.5,151V150"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M172.5,151V150"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M172.5,151L172.5,236"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M171.5,151L171.5,236"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M37.5,236L37.5,170"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M233.5,236L233.5,170"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M163,150.5C163,147.02 162.32,143.57 160.98,140.36C159.65,137.14 157.7,134.22 155.24,131.76C152.78,129.3 149.86,127.35 146.64,126.02C143.43,124.68 139.98,124 136.5,124C133.02,124 129.57,124.68 126.36,126.02C123.14,127.35 120.22,129.3 117.76,131.76C115.3,134.22 113.35,137.14 112.02,140.36C110.68,143.57 110,147.02 110,150.5L136.5,150.5L163,150.5Z"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M110,150.5C110,153.98 110.68,157.43 112.02,160.64C113.35,163.86 115.3,166.78 117.76,169.24C120.22,171.7 123.14,173.65 126.36,174.98C129.57,176.32 133.02,177 136.5,177C139.98,177 143.43,176.32 146.64,174.98C149.86,173.65 152.78,171.7 155.24,169.24C157.7,166.78 159.65,163.86 160.98,160.64C162.32,157.43 163,153.98 163,150.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M177,173.5L172,173.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M177,185.5L172,185.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M177,197.5L172,197.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M177,209.5L172,209.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M100,173.5L95,173.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M100,185.5L95,185.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M100,197.5L95,197.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M100,209.5L95,209.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M233.61,168.79C225.61,149.95 211.97,133.85 194.43,122.57C176.9,111.29 156.27,105.35 135.21,105.5C114.15,105.65 93.62,111.9 76.26,123.43C58.9,134.96 45.51,151.24 37.81,170.2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M233.5,169V168"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M233.5,170V169"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M118.5,218.5C118.5,214 120.34,209.67 123.62,206.48C126.9,203.29 131.35,201.5 136,201.5C140.65,201.5 145.1,203.29 148.38,206.48C151.66,209.67 153.5,214 153.5,218.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M136.5,221.5m0,3a3,3 0,1 1,0 -6a3,3 0,1 1,0 6"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M149,225.5L123,225.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M37.5,171V170"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M38.15,169.77C38.12,169.78 38.14,169.9 38.13,169.93C38.13,170.02 38.07,170.11 38.07,170.21C38.07,170.24 38.04,170.33 38.03,170.37C38.01,170.42 37.97,170.48 37.97,170.54C37.96,170.6 37.91,170.63 37.89,170.69C37.87,170.73 37.84,170.8 37.81,170.82C37.78,170.84 37.76,170.94 37.74,170.97C37.69,171.04 37.63,171.12 37.59,171.19"
android:strokeWidth="0.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M37.99,169.07C37.99,169.09 37.99,169.11 37.99,169.13C37.99,169.16 37.98,169.19 37.96,169.21C37.89,169.32 37.8,169.4 37.72,169.5C37.62,169.61 37.51,169.73 37.45,169.87C37.42,169.95 37.39,170.04 37.36,170.12C37.35,170.15 37.31,170.21 37.33,170.25"
android:strokeWidth="0.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
</vector>

@ -0,0 +1,301 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="567dp"
android:height="269dp"
android:viewportWidth="567"
android:viewportHeight="269">
<path
android:pathData="M73,24L495,24"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M494,23L494,247"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M495,248L73,248"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M72,249L72,23"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M283.5,23L283.5,247"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M283.5,135.5m-27,0a27,27 0,1 1,54 0a27,27 0,1 1,-54 0"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M73,99.5L158,99.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M73,100.5L158,100.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M158.5,172V100"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M158,99.5H159"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M158,172.5H159"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M158,172.5L73,172.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M158,171.5L73,171.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M73,37.5L139,37.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M73,233.5L139,233.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M158.5,163C161.98,163 165.43,162.32 168.64,160.98C171.86,159.65 174.78,157.7 177.24,155.24C179.7,152.78 181.65,149.86 182.98,146.64C184.32,143.43 185,139.98 185,136.5C185,133.02 184.32,129.57 182.98,126.36C181.65,123.14 179.7,120.22 177.24,117.76C174.78,115.3 171.86,113.35 168.64,112.02C165.43,110.68 161.98,110 158.5,110L158.5,136.5L158.5,163Z"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M158.5,110C155.02,110 151.57,110.68 148.36,112.02C145.14,113.35 142.22,115.3 139.76,117.76C137.3,120.22 135.35,123.14 134.02,126.36C132.68,129.57 132,133.02 132,136.5C132,139.98 132.68,143.43 134.02,146.64C135.35,149.86 137.3,152.78 139.76,155.24C142.22,157.7 145.14,159.65 148.36,160.98C151.57,162.32 155.02,163 158.5,163"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M135.5,177L135.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M123.5,177L123.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M111.5,177L111.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M99.5,177L99.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M135.5,100L135.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M123.5,100L123.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M111.5,100L111.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M99.5,100L99.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M140.21,233.61C159.05,225.61 175.15,211.97 186.43,194.43C197.71,176.9 203.65,156.27 203.5,135.21C203.35,114.15 197.1,93.62 185.57,76.26C174.04,58.9 157.76,45.51 138.8,37.81"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M140,233.5H141"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M139,233.5H140"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M90.5,118.5C95,118.5 99.33,120.34 102.52,123.62C105.71,126.9 107.5,131.35 107.5,136C107.5,140.65 105.71,145.1 102.52,148.38C99.33,151.66 95,153.5 90.5,153.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M87.5,136.5m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M83.5,149L83.5,123"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M494,99.5L409,99.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M494,100.5L409,100.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M408.5,172V100"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M409,99.5H408"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M409,172.5H408"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M409,172.5L494,172.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M409,171.5L494,171.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M494,37.5L428,37.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M494,233.5L428,233.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M408.5,163C405.02,163 401.57,162.32 398.36,160.98C395.14,159.65 392.22,157.7 389.76,155.24C387.3,152.78 385.35,149.86 384.02,146.64C382.68,143.43 382,139.98 382,136.5C382,133.02 382.68,129.57 384.02,126.36C385.35,123.14 387.3,120.22 389.76,117.76C392.22,115.3 395.14,113.35 398.36,112.02C401.57,110.68 405.02,110 408.5,110L408.5,136.5L408.5,163Z"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M408.5,110C411.98,110 415.43,110.68 418.64,112.02C421.86,113.35 424.78,115.3 427.24,117.76C429.7,120.22 431.65,123.14 432.98,126.36C434.32,129.57 435,133.02 435,136.5C435,139.98 434.32,143.43 432.98,146.64C431.65,149.86 429.7,152.78 427.24,155.24C424.78,157.7 421.86,159.65 418.64,160.98C415.43,162.32 411.98,163 408.5,163"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M431.5,177L431.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M443.5,177L443.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M455.5,177L455.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M467.5,177L467.5,172"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M431.5,100L431.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M443.5,100L443.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M455.5,100L455.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M467.5,100L467.5,95"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M426.79,233.61C407.95,225.61 391.85,211.97 380.57,194.43C369.3,176.9 363.35,156.27 363.5,135.21C363.65,114.15 369.9,93.62 381.43,76.26C392.95,58.9 409.24,45.51 428.2,37.81"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M427,233.5H426"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M428,233.5H427"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M476.5,118.5C472,118.5 467.67,120.34 464.48,123.62C461.29,126.9 459.5,131.35 459.5,136C459.5,140.65 461.29,145.1 464.48,148.38C467.67,151.66 472,153.5 476.5,153.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M479.5,136.5m3,0a3,3 0,1 0,-6 0a3,3 0,1 0,6 0"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M483.5,149L483.5,123"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:strokeWidth="1"
android:pathData="M138,37.5H139"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M139.23,38.15C139.22,38.12 139.1,38.14 139.07,38.13C138.98,38.13 138.89,38.07 138.79,38.07C138.76,38.07 138.67,38.04 138.63,38.03C138.58,38.01 138.52,37.97 138.46,37.97C138.4,37.96 138.37,37.91 138.31,37.89C138.27,37.87 138.2,37.84 138.18,37.81C138.16,37.78 138.06,37.76 138.03,37.74C137.96,37.69 137.88,37.63 137.81,37.59"
android:strokeWidth="0.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M139.93,37.99C139.91,37.99 139.89,37.99 139.87,37.99C139.84,37.99 139.81,37.98 139.79,37.96C139.68,37.89 139.6,37.8 139.5,37.72C139.39,37.62 139.27,37.51 139.13,37.45C139.05,37.42 138.96,37.39 138.88,37.36C138.85,37.35 138.79,37.31 138.75,37.33"
android:strokeWidth="0.5"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

@ -1,25 +1,36 @@
[versions] [versions]
agp = "8.2.2" agp = "8.2.2"
arrowCore = "1.2.1" arrowCore = "1.2.1"
composeFreeScroll = "0.2.2"
converterGson = "2.9.0" converterGson = "2.9.0"
converterMoshi = "2.5.0"
converterMoshiVersion = "2.5.0"
graphicsShapes = "1.0.0-alpha05"
kotlin = "1.9.0" kotlin = "1.9.0"
coreKtx = "1.10.1" coreKtx = "1.12.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"
espressoCore = "3.5.1" espressoCore = "3.5.1"
kotlinxDatetime = "0.3.2" kotlinxDatetime = "0.3.2"
kotlinxSerializationJsonJvm = "1.6.3" kotlinxSerializationJsonJvm = "1.6.3"
lifecycleRuntimeKtx = "2.6.1" lifecycleRuntimeKtx = "2.7.0"
activityCompose = "1.7.0" activityCompose = "1.8.2"
composeBom = "2023.08.00" composeBom = "2024.02.02"
moshi = "1.15.1"
moshiAdapters = "1.15.1"
moshiKotlin = "1.15.1"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofit2KotlinxSerializationConverter = "1.0.0" retrofit2KotlinxSerializationConverter = "1.0.0"
retrofitAdaptersArrow = "1.0.9" retrofitAdaptersArrow = "1.0.9"
navigationCompose = "2.7.7"
zoomable = "1.6.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } compose-free-scroll = { module = "com.github.chihsuanwu:compose-free-scroll", version.ref = "composeFreeScroll" }
converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 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-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@ -34,10 +45,14 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlinxSerializationJsonJvm" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshiAdapters" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-adapters-arrow = { module = "com.github.skydoves:retrofit-adapters-arrow", version.ref = "retrofitAdaptersArrow" } 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" } 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] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }

@ -1,3 +1,5 @@
import java.net.URI
pluginManagement { pluginManagement {
repositories { repositories {
google { google {
@ -16,6 +18,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = URI.create("https://jitpack.io") }
} }
} }

Loading…
Cancel
Save