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()
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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