Compare commits

...

10 Commits

Author SHA1 Message Date
herel 765fe51720 ui: make filter card collapsible with 2x2 grid layout
- Collapsed: compact row showing 'Filtres' + active filter count + expand arrow
- Expanded: 2x2 grid (Compte+Type, Age+Sous-groupe) halves vertical space
- Animated expand/collapse with AnimatedVisibility
- FilterDropdown now respects weight() modifier for row layout
2026-07-08 18:20:40 +02:00
herel c4bf05d728 feat: add app launcher icon from Flutter project 2026-07-08 18:16:33 +02:00
herel b291277d4f build: remove iosX64 target (unsupported by Compose MP 1.11.1) 2026-07-08 18:12:53 +02:00
herel cc4b0a1ecf fix: chunk large cached events to avoid Preferences 8KB limit on desktop
java.util.prefs.Preferences (desktop Settings backend) has an 8KB
per-value limit. Schedule JSON exceeds this. Now splits into 8000-char
chunks stored as cached_events_chunk_<account>_<i>, with a count key.
Maintains backward compat with old single-value format.
2026-07-08 18:12:19 +02:00
herel d93cae0816 fix: trigger loadAccounts() on auth and loadCachedSchedule() on account selection
The schedule screen was never loading data because no LaunchedEffect
called loadAccounts() when the user became authenticated. Now:
- App.kt: calls scheduleViewModel.loadAccounts() when authState becomes Authenticated
- ScheduleScreen.kt: calls loadCachedSchedule() when selectedAccount is set
2026-07-08 18:07:21 +02:00
herel b676c768fe fix: add back navigation from EventDetailScreen to Schedule 2026-07-08 18:02:14 +02:00
herel 28de93263b feat: wire up app root, navigation, and platform entry points
- Add App.kt root composable with state-based routing
  (Login -> Schedule -> EventDetail) driven by AuthViewModel.state
- Update MainActivity (Android), MainViewController (iOS), and
  Main.kt (Desktop) to host App() with their platform OAuthClient
- Add iOS Xcode source files (MyIceApp.swift, ContentView.swift)
  and Info.plist with myice:// URL scheme
- Delete Placeholder.kt
- Add kotlinx-coroutines-swing to desktopMain to provide
  Dispatchers.Main for viewModelScope (app crashed on desktop
  without it)
2026-07-08 18:00:57 +02:00
herel 88aa9de4ad fix: use French labels for refresh/logout buttons in ScheduleScreen 2026-07-08 17:56:01 +02:00
herel 38d420524a feat: add LoginScreen, ScheduleScreen, and EventDetailScreen
Add 3 Compose Multiplatform screens consuming Task 6 ViewModels and
Task 8 components:
- LoginScreen: MyIce title, login button, loading/error states
- ScheduleScreen: AppBar + filter card (4 dropdowns) + event list
  with loading/error/empty/refreshing states
- EventDetailScreen: AppBar + recap card + players/staff sections
  with amber empty-state card

Use TextButton for Refresh/Logout actions (material-icons-core not
available in this Compose Multiplatform setup). Guard setAccount call
against nullable onSelect callback.
2026-07-08 17:54:19 +02:00
herel 277517bf7e fix: make parseHexColor crash-safe, clean up imports in EventCard 2026-07-08 17:53:04 +02:00
22 changed files with 718 additions and 15 deletions
+1 -1
View File
@@ -18,7 +18,6 @@ kotlin {
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
@@ -59,6 +58,7 @@ kotlin {
val desktopMain by getting {
dependencies {
implementation(libs.ktor.client.cio)
implementation(libs.kotlinx.coroutines.swing)
implementation(compose.desktop.currentOs)
}
}
@@ -5,6 +5,7 @@
<application
android:label="MyIce"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
@@ -4,14 +4,14 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.Text
import ch.parano.myice.auth.AndroidOAuthClient
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
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) {
companion object {
private const val CHUNK_SIZE = 8000
}
private val json = Json { ignoreUnknownKeys = true }
private val eventListSerializer = ListSerializer(Event.serializer())
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 subgroupKey(account: String) = "filters_subgroup_$account"
private fun typeFilterKey(account: String) = "filters_typefilter_$account"
@@ -20,11 +26,31 @@ class ScheduleCache(private val settings: Settings) {
@OptIn(ExperimentalTime::class)
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())
}
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
return try {
json.decodeFromString(eventListSerializer, jsonString)
@@ -58,6 +84,11 @@ class ScheduleCache(private val settings: Settings) {
}
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(timestampKey(account))
settings.remove(agegroupKey(account))
@@ -1,6 +1,7 @@
package ch.parano.myice.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -13,7 +14,6 @@ import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@@ -47,7 +47,7 @@ fun EventCard(
modifier = Modifier
.padding(16.dp)
.fillMaxHeight(),
verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = "${event.agegroup} - ${event.name}",
@@ -74,8 +74,7 @@ fun EventCard(
}
}
private fun parseHexColor(hex: String): Color {
val cleanHex = hex.removePrefix("#")
val value = cleanHex.toLong(16)
private fun parseHexColor(hex: String): Color? {
val value = hex.removePrefix("#").toLongOrNull(16) ?: return null
return Color(0xFF000000 or value)
}
@@ -28,7 +28,7 @@ fun FilterDropdown(
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
modifier = modifier.fillMaxWidth(),
modifier = modifier,
) {
OutlinedTextField(
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
import androidx.compose.material3.Text
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import ch.parano.myice.auth.DesktopOAuthClient
fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "MyIce") {
Text("MyIce")
App(oauthClient = DesktopOAuthClient())
}
}
@@ -1,8 +1,8 @@
package ch.parano.myice
import androidx.compose.material3.Text
import androidx.compose.ui.window.ComposeUIViewController
import ch.parano.myice.auth.IosOAuthClient
fun MainViewController() = ComposeUIViewController {
Text("MyIce")
App(oauthClient = IosOAuthClient())
}
+1
View File
@@ -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-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-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
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-no-arg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" }
+10
View File
@@ -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) {}
}
+55
View File
@@ -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>
+11
View File
@@ -0,0 +1,11 @@
import SwiftUI
import ComposeApp
@main
struct MyIceApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}