Compare commits
10 Commits
af85605dd4
...
765fe51720
| Author | SHA1 | Date | |
|---|---|---|---|
|
765fe51720
|
|||
|
c4bf05d728
|
|||
|
b291277d4f
|
|||
|
cc4b0a1ecf
|
|||
|
d93cae0816
|
|||
|
b676c768fe
|
|||
|
28de93263b
|
|||
|
88aa9de4ad
|
|||
|
38d420524a
|
|||
|
277517bf7e
|
@@ -18,7 +18,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
iosX64()
|
|
||||||
iosArm64()
|
iosArm64()
|
||||||
iosSimulatorArm64()
|
iosSimulatorArm64()
|
||||||
|
|
||||||
@@ -59,6 +58,7 @@ kotlin {
|
|||||||
val desktopMain by getting {
|
val desktopMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="MyIce"
|
android:label="MyIce"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.material3.Text
|
import ch.parano.myice.auth.AndroidOAuthClient
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
Text("MyIce")
|
App(oauthClient = AndroidOAuthClient(applicationContext))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
package ch.parano.myice
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import ch.parano.myice.auth.AuthSettings
|
||||||
|
import ch.parano.myice.auth.OAuthClient
|
||||||
|
import ch.parano.myice.cache.ScheduleCache
|
||||||
|
import ch.parano.myice.network.ApiService
|
||||||
|
import ch.parano.myice.network.createHttpClient
|
||||||
|
import ch.parano.myice.ui.screens.EventDetailScreen
|
||||||
|
import ch.parano.myice.ui.screens.LoginScreen
|
||||||
|
import ch.parano.myice.ui.screens.ScheduleScreen
|
||||||
|
import ch.parano.myice.ui.theme.MyIceTheme
|
||||||
|
import ch.parano.myice.viewmodel.AuthState
|
||||||
|
import ch.parano.myice.viewmodel.AuthViewModel
|
||||||
|
import ch.parano.myice.viewmodel.EventDetailViewModel
|
||||||
|
import ch.parano.myice.viewmodel.ScheduleViewModel
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
|
|
||||||
|
sealed class Screen {
|
||||||
|
data object Login : Screen()
|
||||||
|
data object Schedule : Screen()
|
||||||
|
data class EventDetail(val gameId: String, val account: String, val eventTitle: String) : Screen()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun App(
|
||||||
|
oauthClient: OAuthClient,
|
||||||
|
) {
|
||||||
|
val apiService = remember { ApiService(createHttpClient()) }
|
||||||
|
val authSettings = remember { AuthSettings(Settings()) }
|
||||||
|
val cache = remember { ScheduleCache(Settings()) }
|
||||||
|
|
||||||
|
val authViewModel = remember { AuthViewModel(apiService, authSettings, oauthClient) }
|
||||||
|
val scheduleViewModel = remember { ScheduleViewModel(apiService, cache, authSettings) }
|
||||||
|
val eventDetailViewModel = remember { EventDetailViewModel(apiService) }
|
||||||
|
|
||||||
|
var currentScreen by remember { mutableStateOf<Screen>(Screen.Login) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
authViewModel.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
val authState by authViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(authState) {
|
||||||
|
currentScreen = when (authState) {
|
||||||
|
is AuthState.Authenticated -> Screen.Schedule
|
||||||
|
is AuthState.Unauthenticated -> Screen.Login
|
||||||
|
AuthState.Loading -> currentScreen
|
||||||
|
}
|
||||||
|
if (authState is AuthState.Authenticated) {
|
||||||
|
scheduleViewModel.loadAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MyIceTheme {
|
||||||
|
when (val screen = currentScreen) {
|
||||||
|
Screen.Login -> LoginScreen(viewModel = authViewModel)
|
||||||
|
Screen.Schedule -> ScheduleScreen(
|
||||||
|
scheduleViewModel = scheduleViewModel,
|
||||||
|
authViewModel = authViewModel,
|
||||||
|
onEventClick = { gameId, account, eventTitle ->
|
||||||
|
currentScreen = Screen.EventDetail(gameId, account, eventTitle)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
is Screen.EventDetail -> EventDetailScreen(
|
||||||
|
gameId = screen.gameId,
|
||||||
|
account = screen.account,
|
||||||
|
eventTitle = screen.eventTitle,
|
||||||
|
viewModel = eventDetailViewModel,
|
||||||
|
onBack = { currentScreen = Screen.Schedule },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
package ch.parano.myice
|
|
||||||
@@ -9,10 +9,16 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
class ScheduleCache(private val settings: Settings) {
|
class ScheduleCache(private val settings: Settings) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHUNK_SIZE = 8000
|
||||||
|
}
|
||||||
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
private val eventListSerializer = ListSerializer(Event.serializer())
|
private val eventListSerializer = ListSerializer(Event.serializer())
|
||||||
|
|
||||||
private fun eventsKey(account: String) = "cached_events_$account"
|
private fun eventsKey(account: String) = "cached_events_$account"
|
||||||
|
private fun eventsChunkCountKey(account: String) = "cached_events_count_$account"
|
||||||
|
private fun eventsChunkKey(account: String, index: Int) = "cached_events_chunk_${account}_$index"
|
||||||
private fun agegroupKey(account: String) = "filters_agegroup_$account"
|
private fun agegroupKey(account: String) = "filters_agegroup_$account"
|
||||||
private fun subgroupKey(account: String) = "filters_subgroup_$account"
|
private fun subgroupKey(account: String) = "filters_subgroup_$account"
|
||||||
private fun typeFilterKey(account: String) = "filters_typefilter_$account"
|
private fun typeFilterKey(account: String) = "filters_typefilter_$account"
|
||||||
@@ -20,11 +26,31 @@ class ScheduleCache(private val settings: Settings) {
|
|||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
fun saveEvents(account: String, events: List<Event>) {
|
fun saveEvents(account: String, events: List<Event>) {
|
||||||
settings.putString(eventsKey(account), json.encodeToString(eventListSerializer, events))
|
val jsonStr = json.encodeToString(eventListSerializer, events)
|
||||||
|
val chunks = jsonStr.chunked(CHUNK_SIZE)
|
||||||
|
|
||||||
|
settings.remove(eventsKey(account))
|
||||||
|
settings.putInt(eventsChunkCountKey(account), chunks.size)
|
||||||
|
chunks.forEachIndexed { i, chunk ->
|
||||||
|
settings.putString(eventsChunkKey(account, i), chunk)
|
||||||
|
}
|
||||||
settings.putLong(timestampKey(account), Clock.System.now().toEpochMilliseconds())
|
settings.putLong(timestampKey(account), Clock.System.now().toEpochMilliseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEvents(account: String): List<Event>? {
|
fun loadEvents(account: String): List<Event>? {
|
||||||
|
val chunkCount = settings.getIntOrNull(eventsChunkCountKey(account))
|
||||||
|
if (chunkCount != null && chunkCount > 0) {
|
||||||
|
val jsonStr = (0 until chunkCount).mapNotNull { i ->
|
||||||
|
settings.getStringOrNull(eventsChunkKey(account, i))
|
||||||
|
}.joinToString("")
|
||||||
|
if (jsonStr.isEmpty()) return null
|
||||||
|
return try {
|
||||||
|
json.decodeFromString(eventListSerializer, jsonStr)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val jsonString = settings.getStringOrNull(eventsKey(account)) ?: return null
|
val jsonString = settings.getStringOrNull(eventsKey(account)) ?: return null
|
||||||
return try {
|
return try {
|
||||||
json.decodeFromString(eventListSerializer, jsonString)
|
json.decodeFromString(eventListSerializer, jsonString)
|
||||||
@@ -58,6 +84,11 @@ class ScheduleCache(private val settings: Settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clearCache(account: String) {
|
fun clearCache(account: String) {
|
||||||
|
val chunkCount = settings.getIntOrNull(eventsChunkCountKey(account)) ?: 0
|
||||||
|
for (i in 0 until chunkCount) {
|
||||||
|
settings.remove(eventsChunkKey(account, i))
|
||||||
|
}
|
||||||
|
settings.remove(eventsChunkCountKey(account))
|
||||||
settings.remove(eventsKey(account))
|
settings.remove(eventsKey(account))
|
||||||
settings.remove(timestampKey(account))
|
settings.remove(timestampKey(account))
|
||||||
settings.remove(agegroupKey(account))
|
settings.remove(agegroupKey(account))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ch.parano.myice.ui.components
|
package ch.parano.myice.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -13,7 +14,6 @@ import androidx.compose.material3.Card
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -47,7 +47,7 @@ fun EventCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${event.agegroup} - ${event.name}",
|
text = "${event.agegroup} - ${event.name}",
|
||||||
@@ -74,8 +74,7 @@ fun EventCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseHexColor(hex: String): Color {
|
private fun parseHexColor(hex: String): Color? {
|
||||||
val cleanHex = hex.removePrefix("#")
|
val value = hex.removePrefix("#").toLongOrNull(16) ?: return null
|
||||||
val value = cleanHex.toLong(16)
|
|
||||||
return Color(0xFF000000 or value)
|
return Color(0xFF000000 or value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ fun FilterDropdown(
|
|||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandedChange = { expanded = it },
|
onExpandedChange = { expanded = it },
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = selectedValue ?: "",
|
value = selectedValue ?: "",
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package ch.parano.myice.ui.screens
|
||||||
|
|
||||||
|
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.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ch.parano.myice.ui.components.LoadingIndicator
|
||||||
|
import ch.parano.myice.viewmodel.EventDetailViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun EventDetailScreen(
|
||||||
|
gameId: String,
|
||||||
|
account: String,
|
||||||
|
eventTitle: String,
|
||||||
|
viewModel: EventDetailViewModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LaunchedEffect(gameId, account) {
|
||||||
|
viewModel.loadEventDetail(gameId, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(eventTitle) },
|
||||||
|
navigationIcon = {
|
||||||
|
androidx.compose.material3.TextButton(onClick = onBack) {
|
||||||
|
Text("Retour")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
) { padding ->
|
||||||
|
when {
|
||||||
|
state.isLoading -> {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(padding))
|
||||||
|
}
|
||||||
|
state.error != null -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.error!!,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
TextButton(onClick = { viewModel.loadEventDetail(gameId, account) }) {
|
||||||
|
Text("Reessayer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.eventDetail != null -> {
|
||||||
|
val detail = state.eventDetail!!
|
||||||
|
val hasPlayers = detail.convocation.available.isNotEmpty()
|
||||||
|
val hasStaff = detail.convocation.staff.isNotEmpty()
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text("Type: ${detail.type}")
|
||||||
|
Text("Lieu: ${detail.place}")
|
||||||
|
Text("Heure: ${detail.timeStart} - ${detail.timeEnd}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPlayers && !hasStaff) {
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = Color(0xFFFFC107),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Aucun joueur ni personnel convoque",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPlayers) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Joueurs (${detail.convocation.available.size})",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(detail.convocation.available) { player ->
|
||||||
|
val position = player.position ?: "N/A"
|
||||||
|
val number = player.number ?: "N/A"
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text("[$position] #$number - ${player.fname} ${player.lname}")
|
||||||
|
Text(
|
||||||
|
text = "DOB: ${player.dob}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStaff) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Personnel",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(detail.convocation.staff) { staff ->
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = "${staff.role}: ${staff.fname} ${staff.lname}",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text("Aucune donnee disponible")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package ch.parano.myice.ui.screens
|
||||||
|
|
||||||
|
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.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ch.parano.myice.viewmodel.AuthViewModel
|
||||||
|
import ch.parano.myice.viewmodel.AuthState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(
|
||||||
|
viewModel: AuthViewModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
LaunchedEffect(state) {
|
||||||
|
if (state is AuthState.Unauthenticated) {
|
||||||
|
snackbarHostState.showSnackbar("Erreur de connexion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
modifier = modifier,
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "MyIce",
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Games",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
if (state is AuthState.Loading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.login() },
|
||||||
|
) {
|
||||||
|
Text("Se connecter avec Infomaniak")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package ch.parano.myice.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import ch.parano.myice.filter.TypeFilter
|
||||||
|
import ch.parano.myice.ui.components.EventCard
|
||||||
|
import ch.parano.myice.ui.components.FilterDropdown
|
||||||
|
import ch.parano.myice.ui.components.LoadingIndicator
|
||||||
|
import ch.parano.myice.viewmodel.AuthViewModel
|
||||||
|
import ch.parano.myice.viewmodel.ScheduleViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ScheduleScreen(
|
||||||
|
scheduleViewModel: ScheduleViewModel,
|
||||||
|
authViewModel: AuthViewModel,
|
||||||
|
onEventClick: (gameId: String, account: String, eventTitle: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheduleState by scheduleViewModel.state.collectAsState()
|
||||||
|
val authState by authViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(scheduleState.selectedAccount) {
|
||||||
|
if (scheduleState.selectedAccount != null && scheduleState.events.isEmpty()) {
|
||||||
|
scheduleViewModel.loadCachedSchedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("MyIce") },
|
||||||
|
actions = {
|
||||||
|
val email = (authState as? ch.parano.myice.viewmodel.AuthState.Authenticated)?.email
|
||||||
|
if (email != null) {
|
||||||
|
Text(
|
||||||
|
text = email,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(onClick = { scheduleViewModel.refreshSchedule() }) {
|
||||||
|
Text("Rafraichir")
|
||||||
|
}
|
||||||
|
TextButton(onClick = {
|
||||||
|
scheduleViewModel.clearCache()
|
||||||
|
authViewModel.logout()
|
||||||
|
}) {
|
||||||
|
Text("Deconnexion")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
) {
|
||||||
|
var filtersExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val activeFilterCount = listOf(
|
||||||
|
scheduleState.selectedAccount,
|
||||||
|
scheduleState.selectedAgegroup,
|
||||||
|
scheduleState.selectedSubgroup,
|
||||||
|
).count { it != null } + if (scheduleState.typeFilter != TypeFilter.GAMES) 1 else 0
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { filtersExpanded = !filtersExpanded }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Filtres",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
if (activeFilterCount > 0) {
|
||||||
|
Spacer(Modifier.height(0.dp))
|
||||||
|
Text(
|
||||||
|
text = "($activeFilterCount actif${if (activeFilterCount > 1) "s" else ""})",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = if (filtersExpanded) "▲" else "▼",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = filtersExpanded,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
FilterDropdown(
|
||||||
|
label = "Compte",
|
||||||
|
selectedValue = scheduleState.selectedAccount,
|
||||||
|
options = scheduleState.accounts.map { it.name },
|
||||||
|
onSelect = { it?.let { acc -> scheduleViewModel.setAccount(acc) } },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
FilterDropdown(
|
||||||
|
label = "Type",
|
||||||
|
selectedValue = typeFilterLabel(scheduleState.typeFilter),
|
||||||
|
options = listOf("Tous", "Matchs", "Entrainements"),
|
||||||
|
onSelect = { label ->
|
||||||
|
val filter = when (label) {
|
||||||
|
"Tous" -> TypeFilter.ALL
|
||||||
|
"Matchs" -> TypeFilter.GAMES
|
||||||
|
"Entrainements" -> TypeFilter.PRACTICES
|
||||||
|
else -> TypeFilter.DEFAULT
|
||||||
|
}
|
||||||
|
scheduleViewModel.setTypeFilter(filter)
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
FilterDropdown(
|
||||||
|
label = "Age",
|
||||||
|
selectedValue = scheduleState.selectedAgegroup,
|
||||||
|
options = scheduleState.agegroups,
|
||||||
|
onSelect = { scheduleViewModel.setAgegroup(it) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
FilterDropdown(
|
||||||
|
label = "Sous-groupe",
|
||||||
|
selectedValue = scheduleState.selectedSubgroup,
|
||||||
|
options = scheduleState.subgroups,
|
||||||
|
onSelect = { scheduleViewModel.setSubgroup(it) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
scheduleState.isLoading && scheduleState.events.isEmpty() -> {
|
||||||
|
LoadingIndicator()
|
||||||
|
}
|
||||||
|
scheduleState.error != null && scheduleState.events.isEmpty() -> {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = scheduleState.error!!,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
TextButton(onClick = { scheduleViewModel.refreshSchedule() }) {
|
||||||
|
Text("Reessayer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleState.filteredEvents.isEmpty() -> {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text("Aucun evenement disponible")
|
||||||
|
if (scheduleState.events.isEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Appuyez sur le bouton de rafraichissement pour charger les evenements",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
items(scheduleState.filteredEvents) { event ->
|
||||||
|
EventCard(
|
||||||
|
event = event,
|
||||||
|
onClick = {
|
||||||
|
onEventClick(
|
||||||
|
event.idEvent,
|
||||||
|
scheduleState.selectedAccount!!,
|
||||||
|
event.title,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scheduleState.isRefreshing) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Card {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text("Chargement...", fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typeFilterLabel(filter: TypeFilter): String = when (filter) {
|
||||||
|
TypeFilter.ALL -> "Tous"
|
||||||
|
TypeFilter.GAMES -> "Matchs"
|
||||||
|
TypeFilter.PRACTICES -> "Entrainements"
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package ch.parano.myice
|
package ch.parano.myice
|
||||||
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
|
import ch.parano.myice.auth.DesktopOAuthClient
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
Window(onCloseRequest = ::exitApplication, title = "MyIce") {
|
Window(onCloseRequest = ::exitApplication, title = "MyIce") {
|
||||||
Text("MyIce")
|
App(oauthClient = DesktopOAuthClient())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package ch.parano.myice
|
package ch.parano.myice
|
||||||
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.ui.window.ComposeUIViewController
|
import androidx.compose.ui.window.ComposeUIViewController
|
||||||
|
import ch.parano.myice.auth.IosOAuthClient
|
||||||
|
|
||||||
fun MainViewController() = ComposeUIViewController {
|
fun MainViewController() = ComposeUIViewController {
|
||||||
Text("MyIce")
|
App(oauthClient = IosOAuthClient())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "kto
|
|||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" }
|
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" }
|
||||||
multiplatform-settings-no-arg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" }
|
multiplatform-settings-no-arg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" }
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
struct ContentView: UIViewRepresentable {
|
||||||
|
func makeUIView(context: Context) -> some UIView {
|
||||||
|
MainViewControllerKt.MainViewController()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIViewType, context: Context) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>ch.parano.myice</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>myice</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct MyIceApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user