Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c9716a352 | ||
|
|
f2f5a15967 | ||
|
|
22c0a61e32 | ||
|
|
057be15822 | ||
|
|
8f5d5d1ca8 | ||
|
|
cd58d58b8f
|
||
|
|
3caa608d97
|
||
|
|
4a439bfd2c
|
||
|
|
4bc40e9128
|
||
|
|
77c8e65be7
|
||
|
|
85e2391833
|
||
|
|
c9841b6744
|
||
|
|
d03ac82a8a
|
||
|
|
3edf991cde
|
||
|
|
73b8e4480a
|
||
|
|
c1c8e21ba3
|
||
|
|
e5c0a1183d
|
||
|
|
06d2eceba3
|
||
|
|
a7e3b52ca6
|
||
|
|
cf24f9c0e3
|
||
|
|
b7af237002
|
||
|
|
be6e9d733a
|
||
|
|
d4f43de933
|
||
|
|
efaa93d907
|
||
|
|
418a8978f2
|
||
|
|
a2d5a57aec
|
||
|
|
ea15c0423c
|
||
|
|
3dc5890a7a
|
||
|
|
fd8fcb9b4c
|
||
|
|
1e5c2fd864
|
||
|
|
9524bc93ca
|
||
|
|
1795337d01 |
@@ -3,7 +3,6 @@
|
|||||||
## Project
|
## Project
|
||||||
|
|
||||||
MyIce — Kotlin Multiplatform ice hockey schedule/convocation viewer (Android, iOS, Desktop).
|
MyIce — Kotlin Multiplatform ice hockey schedule/convocation viewer (Android, iOS, Desktop).
|
||||||
Port of the Flutter app at `~/work/gitea.parano.ch/herel/myice/myice_mobile/`.
|
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.PHONY: android desktop desktop-install android-usb android-emulator
|
||||||
|
|
||||||
|
ADB := $(shell command -v adb 2>/dev/null || echo "$(HOME)/Library/Android/sdk/platform-tools/adb")
|
||||||
|
EMULATOR := $(shell command -v emulator 2>/dev/null || echo "$(HOME)/Library/Android/sdk/emulator/emulator")
|
||||||
|
AVD := $(firstword $(shell $(EMULATOR) -list-avds 2>/dev/null))
|
||||||
|
APK := composeApp/build/outputs/apk/debug/composeApp-debug.apk
|
||||||
|
|
||||||
|
android:
|
||||||
|
./gradlew composeApp:assembleDebug
|
||||||
|
|
||||||
|
desktop:
|
||||||
|
./gradlew composeApp:run
|
||||||
|
|
||||||
|
desktop-install:
|
||||||
|
./gradlew composeApp:packageDistributionForCurrentOS
|
||||||
|
rm -rf /Applications/myicek.app
|
||||||
|
cp -R composeApp/build/compose/binaries/main/app/myicek.app /Applications/myicek.app
|
||||||
|
|
||||||
|
android-usb: android
|
||||||
|
$(ADB) -d install -r $(APK)
|
||||||
|
$(ADB) -d shell am start -n ch.parano.myicek/.MainActivity
|
||||||
|
|
||||||
|
android-emulator: android
|
||||||
|
@if ! $(ADB) devices | grep -q '^emulator'; then \
|
||||||
|
echo "Aucun émulateur en cours — lancement de $(AVD)…"; \
|
||||||
|
$(EMULATOR) -avd $(AVD) -no-snapshot-load >/dev/null 2>&1 & \
|
||||||
|
echo "En attente du démarrage…"; \
|
||||||
|
$(ADB) -e wait-for-device; \
|
||||||
|
while [ "$$($(ADB) -e shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do \
|
||||||
|
sleep 2; \
|
||||||
|
done; \
|
||||||
|
echo "Émulateur prêt."; \
|
||||||
|
fi
|
||||||
|
$(ADB) -e install -r $(APK)
|
||||||
|
$(ADB) -e shell am start -n ch.parano.myicek/.MainActivity
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# MyIce Kotlin Multiplatform
|
# MyIce Kotlin Multiplatform
|
||||||
|
|
||||||
Ice hockey schedule and convocation viewer for Android, iOS, and Desktop (macOS/Windows/Linux).
|
Ice hockey schedule and convocation viewer for Android, iOS, and Desktop (macOS/Windows/Linux).
|
||||||
Port of the [Flutter app](https://gitea.parano.ch/herel/myice/myice_mobile) to Kotlin Multiplatform with Compose Multiplatform.
|
Kotlin Multiplatform with Compose Multiplatform.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ kotlin {
|
|||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(compose.components.uiToolingPreview)
|
implementation(compose.components.uiToolingPreview)
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
@@ -83,11 +84,11 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "ch.parano.myice"
|
namespace = "ch.parano.myicek"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "ch.parano.myice"
|
applicationId = "ch.parano.myicek"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
@@ -118,10 +119,10 @@ android {
|
|||||||
|
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
application {
|
application {
|
||||||
mainClass = "ch.parano.myice.MainKt"
|
mainClass = "ch.parano.myicek.MainKt"
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
packageName = "myice"
|
packageName = "myicek"
|
||||||
packageVersion = "1.0.0"
|
packageVersion = "1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
android:label="MyIce"
|
android:label="MyIce"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|||||||
+2
-2
@@ -15,13 +15,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice
|
package ch.parano.myicek
|
||||||
|
|
||||||
import android.os.Bundle
|
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 ch.parano.myice.auth.AndroidOAuthClient
|
import ch.parano.myicek.auth.AndroidOAuthClient
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
+2
-2
@@ -15,12 +15,12 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice
|
package ch.parano.myicek
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import ch.parano.myice.auth.OAuthCallbackHolder
|
import ch.parano.myicek.auth.OAuthCallbackHolder
|
||||||
|
|
||||||
class OAuthCallbackActivity : Activity() {
|
class OAuthCallbackActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.auth
|
package ch.parano.myicek.auth
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
import io.ktor.client.engine.HttpClientEngineConfig
|
import io.ktor.client.engine.HttpClientEngineConfig
|
||||||
import io.ktor.client.engine.HttpClientEngineFactory
|
import io.ktor.client.engine.HttpClientEngineFactory
|
||||||
+6
-8
@@ -15,14 +15,12 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice
|
package ch.parano.myicek.ui
|
||||||
|
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.runtime.Composable
|
||||||
import ch.parano.myice.auth.DesktopOAuthClient
|
|
||||||
|
|
||||||
fun main() = application {
|
@Composable
|
||||||
Window(onCloseRequest = ::exitApplication, title = "MyIce") {
|
actual fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit) {
|
||||||
App(oauthClient = DesktopOAuthClient())
|
BackHandler(enabled = enabled, onBack = onBack)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
|
||||||
// Copyright (C) 2026 parano.ch
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import ch.parano.myice.models.Event
|
|
||||||
import ch.parano.myice.util.formatEventDateTime
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EventCard(
|
|
||||||
event: Event,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val stripeColor = event.color?.let { parseHexColor(it) }
|
|
||||||
?: MaterialTheme.colorScheme.primary
|
|
||||||
|
|
||||||
Card(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(4.dp)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.background(stripeColor),
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "${event.agegroup} - ${event.name}",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = event.title,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Adversaire: ${event.opponent}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Lieu: ${event.place}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = formatEventDateTime(event.start, event.end),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseHexColor(hex: String): Color? {
|
|
||||||
val value = hex.removePrefix("#").toLongOrNull(16) ?: return null
|
|
||||||
return Color(0xFF000000 or value)
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
|
||||||
// Copyright (C) 2026 parano.ch
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package ch.parano.myice.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun FilterDropdown(
|
|
||||||
label: String,
|
|
||||||
selectedValue: String?,
|
|
||||||
options: List<String>,
|
|
||||||
onSelect: (String?) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = { expanded = it },
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = selectedValue ?: "",
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
label = { Text(label) },
|
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
|
||||||
modifier = Modifier.menuAnchor().fillMaxWidth(),
|
|
||||||
)
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false },
|
|
||||||
) {
|
|
||||||
options.forEach { option ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(option) },
|
|
||||||
onClick = {
|
|
||||||
onSelect(option)
|
|
||||||
expanded = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
|
||||||
// Copyright (C) 2026 parano.ch
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
|
||||||
// Copyright (C) 2026 parano.ch
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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,54 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
|
||||||
// Copyright (C) 2026 parano.ch
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package ch.parano.myice.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
private val Indigo = Color(0xFF3F51B5)
|
|
||||||
private val IndigoLight = Color(0xFF7986CB)
|
|
||||||
private val IndigoDark = Color(0xFF303F9F)
|
|
||||||
|
|
||||||
private val LightColors = lightColorScheme(
|
|
||||||
primary = Indigo,
|
|
||||||
onPrimary = Color.White,
|
|
||||||
primaryContainer = IndigoDark,
|
|
||||||
secondary = IndigoLight,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val DarkColors = darkColorScheme(
|
|
||||||
primary = IndigoLight,
|
|
||||||
onPrimary = Color.Black,
|
|
||||||
primaryContainer = Indigo,
|
|
||||||
secondary = Indigo,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MyIceTheme(
|
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
MaterialTheme(
|
|
||||||
colorScheme = if (darkTheme) DarkColors else LightColors,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+39
-18
@@ -15,8 +15,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice
|
package ch.parano.myicek
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -24,25 +29,31 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import ch.parano.myice.auth.AuthSettings
|
import ch.parano.myicek.auth.AuthSettings
|
||||||
import ch.parano.myice.auth.OAuthClient
|
import ch.parano.myicek.auth.OAuthClient
|
||||||
import ch.parano.myice.cache.ScheduleCache
|
import ch.parano.myicek.cache.ScheduleCache
|
||||||
import ch.parano.myice.network.ApiService
|
import ch.parano.myicek.network.ApiService
|
||||||
import ch.parano.myice.network.createHttpClient
|
import ch.parano.myicek.network.createHttpClient
|
||||||
import ch.parano.myice.ui.screens.EventDetailScreen
|
import ch.parano.myicek.ui.screens.EventDetailScreen
|
||||||
import ch.parano.myice.ui.screens.LoginScreen
|
import ch.parano.myicek.ui.screens.LoginScreen
|
||||||
import ch.parano.myice.ui.screens.ScheduleScreen
|
import ch.parano.myicek.ui.screens.ScheduleScreen
|
||||||
import ch.parano.myice.ui.theme.MyIceTheme
|
import ch.parano.myicek.ui.theme.MyIceTheme
|
||||||
import ch.parano.myice.viewmodel.AuthState
|
import ch.parano.myicek.viewmodel.AuthState
|
||||||
import ch.parano.myice.viewmodel.AuthViewModel
|
import ch.parano.myicek.viewmodel.AuthViewModel
|
||||||
import ch.parano.myice.viewmodel.EventDetailViewModel
|
import ch.parano.myicek.viewmodel.EventDetailViewModel
|
||||||
import ch.parano.myice.viewmodel.ScheduleViewModel
|
import ch.parano.myicek.viewmodel.ScheduleViewModel
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
|
|
||||||
sealed class Screen {
|
sealed class Screen {
|
||||||
data object Login : Screen()
|
data object Login : Screen()
|
||||||
data object Schedule : Screen()
|
data object Schedule : Screen()
|
||||||
data class EventDetail(val gameId: String, val account: String, val eventTitle: String) : Screen()
|
data class EventDetail(
|
||||||
|
val gameId: String,
|
||||||
|
val account: String,
|
||||||
|
val eventTitle: String,
|
||||||
|
val eventStart: String,
|
||||||
|
val eventEnd: String,
|
||||||
|
) : Screen()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -77,22 +88,32 @@ fun App(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MyIceTheme {
|
MyIceTheme {
|
||||||
when (val screen = currentScreen) {
|
AnimatedContent(
|
||||||
|
targetState = currentScreen,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
|
||||||
|
},
|
||||||
|
label = "screenTransition",
|
||||||
|
) { screen ->
|
||||||
|
when (screen) {
|
||||||
Screen.Login -> LoginScreen(viewModel = authViewModel)
|
Screen.Login -> LoginScreen(viewModel = authViewModel)
|
||||||
Screen.Schedule -> ScheduleScreen(
|
Screen.Schedule -> ScheduleScreen(
|
||||||
scheduleViewModel = scheduleViewModel,
|
scheduleViewModel = scheduleViewModel,
|
||||||
authViewModel = authViewModel,
|
authViewModel = authViewModel,
|
||||||
onEventClick = { gameId, account, eventTitle ->
|
onEventClick = { gameId, account, eventTitle, eventStart, eventEnd ->
|
||||||
currentScreen = Screen.EventDetail(gameId, account, eventTitle)
|
currentScreen = Screen.EventDetail(gameId, account, eventTitle, eventStart, eventEnd)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
is Screen.EventDetail -> EventDetailScreen(
|
is Screen.EventDetail -> EventDetailScreen(
|
||||||
gameId = screen.gameId,
|
gameId = screen.gameId,
|
||||||
account = screen.account,
|
account = screen.account,
|
||||||
eventTitle = screen.eventTitle,
|
eventTitle = screen.eventTitle,
|
||||||
|
eventStart = screen.eventStart,
|
||||||
|
eventEnd = screen.eventEnd,
|
||||||
viewModel = eventDetailViewModel,
|
viewModel = eventDetailViewModel,
|
||||||
onBack = { currentScreen = Screen.Schedule },
|
onBack = { currentScreen = Screen.Schedule },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek
|
||||||
|
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
|
|
||||||
|
data class SavedWindowState(
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val x: Int,
|
||||||
|
val y: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
class WindowStateStorage(private val settings: Settings) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val WIDTH_KEY = "window_width"
|
||||||
|
private const val HEIGHT_KEY = "window_height"
|
||||||
|
private const val X_KEY = "window_x"
|
||||||
|
private const val Y_KEY = "window_y"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(width: Int, height: Int, x: Int, y: Int) {
|
||||||
|
settings.putInt(WIDTH_KEY, width)
|
||||||
|
settings.putInt(HEIGHT_KEY, height)
|
||||||
|
settings.putInt(X_KEY, x)
|
||||||
|
settings.putInt(Y_KEY, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(): SavedWindowState? {
|
||||||
|
val width = settings.getIntOrNull(WIDTH_KEY) ?: return null
|
||||||
|
val height = settings.getIntOrNull(HEIGHT_KEY) ?: return null
|
||||||
|
val x = settings.getIntOrNull(X_KEY) ?: return null
|
||||||
|
val y = settings.getIntOrNull(Y_KEY) ?: return null
|
||||||
|
return SavedWindowState(width, height, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -15,9 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.auth
|
package ch.parano.myicek.auth
|
||||||
|
|
||||||
import ch.parano.myice.network.AuthTokenHolder
|
import ch.parano.myicek.network.AuthTokenHolder
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
|
|
||||||
class AuthSettings(private val settings: Settings) {
|
class AuthSettings(private val settings: Settings) {
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.auth
|
package ch.parano.myicek.auth
|
||||||
|
|
||||||
interface OAuthClient {
|
interface OAuthClient {
|
||||||
fun redirectUri(): String
|
fun redirectUri(): String
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.auth
|
package ch.parano.myicek.auth
|
||||||
|
|
||||||
internal fun parseAccessTokenFromFragment(fragment: String): String? {
|
internal fun parseAccessTokenFromFragment(fragment: String): String? {
|
||||||
val params = fragment.split("&").associate { pair ->
|
val params = fragment.split("&").associate { pair ->
|
||||||
+2
-2
@@ -15,9 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.cache
|
package ch.parano.myicek.cache
|
||||||
|
|
||||||
import ch.parano.myice.models.Event
|
import ch.parano.myicek.models.Event
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
+8
-2
@@ -15,9 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.filter
|
package ch.parano.myicek.filter
|
||||||
|
|
||||||
import ch.parano.myice.models.Event
|
import ch.parano.myicek.models.Event
|
||||||
|
|
||||||
fun extractSubgroup(name: String): String {
|
fun extractSubgroup(name: String): String {
|
||||||
val parts = name.split(" - ")
|
val parts = name.split(" - ")
|
||||||
@@ -52,3 +52,9 @@ fun List<Event>.distinctAgegroups(): List<String> =
|
|||||||
|
|
||||||
fun List<Event>.distinctSubgroups(): List<String> =
|
fun List<Event>.distinctSubgroups(): List<String> =
|
||||||
map { extractSubgroup(it.name) }.toSet().sorted()
|
map { extractSubgroup(it.name) }.toSet().sorted()
|
||||||
|
|
||||||
|
fun groupEventsByDate(events: List<Event>): List<Pair<String, List<Event>>> =
|
||||||
|
events
|
||||||
|
.sortedBy { it.start }
|
||||||
|
.groupBy { it.start.take(10) }
|
||||||
|
.toList()
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.filter
|
package ch.parano.myicek.filter
|
||||||
|
|
||||||
enum class TypeFilter {
|
enum class TypeFilter {
|
||||||
ALL, GAMES, PRACTICES;
|
ALL, GAMES, PRACTICES;
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
object ApiConfig {
|
object ApiConfig {
|
||||||
const val baseUrl = "https://myice.parano.ch"
|
const val baseUrl = "https://myice.parano.ch"
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.HttpClientEngineConfig
|
import io.ktor.client.engine.HttpClientEngineConfig
|
||||||
+5
-5
@@ -15,12 +15,12 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
import ch.parano.myice.models.Account
|
import ch.parano.myicek.models.Account
|
||||||
import ch.parano.myice.models.Event
|
import ch.parano.myicek.models.Event
|
||||||
import ch.parano.myice.models.EventDetail
|
import ch.parano.myicek.models.EventDetail
|
||||||
import ch.parano.myice.models.UserInfo
|
import ch.parano.myicek.models.UserInfo
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
object AuthTokenHolder {
|
object AuthTokenHolder {
|
||||||
var token: String? = null
|
var token: String? = null
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun PlatformBackHandler(enabled: Boolean = true, onBack: () -> Unit)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.components
|
||||||
|
|
||||||
|
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.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ch.parano.myicek.ui.theme.Dimens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyState(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Dimens.md))
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Dimens.sm))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.components
|
||||||
|
|
||||||
|
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.size
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ch.parano.myicek.ui.theme.Dimens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ErrorState(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Dimens.md))
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Dimens.sm))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Dimens.lg))
|
||||||
|
Button(onClick = onRetry) {
|
||||||
|
Text("Réessayer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.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
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Place
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.AssistChipDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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
|
||||||
|
import ch.parano.myicek.models.Event
|
||||||
|
import ch.parano.myicek.ui.theme.Dimens
|
||||||
|
import ch.parano.myicek.util.formatEventTimeRange
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EventCard(
|
||||||
|
event: Event,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val isGame = event.eventType == "Jeu"
|
||||||
|
val stripeColor = event.color?.let { parseHexColor(it) }
|
||||||
|
?: MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.lg, vertical = Dimens.sm),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(4.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(stripeColor),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Dimens.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.xs),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${event.agegroup} - ${event.name}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
AssistChip(
|
||||||
|
onClick = {},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
if (isGame) "Match" else "Entraînement",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = if (isGame) {
|
||||||
|
AssistChipDefaults.assistChipColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
labelColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AssistChipDefaults.assistChipColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
labelColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = if (isGame) "Match vs ${event.opponent}" else "Entraînement",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Dimens.xs))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.xs),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Place,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.width(18.dp).height(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = event.place,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.xs),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Schedule,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.width(18.dp).height(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatEventTimeRange(event.start, event.end),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseHexColor(hex: String): Color? {
|
||||||
|
val value = hex.removePrefix("#").toLongOrNull(16) ?: return null
|
||||||
|
return Color(0xFF000000 or value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.AssistChipDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import ch.parano.myicek.filter.TypeFilter
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun TypeFilterSegmented(
|
||||||
|
selected: TypeFilter,
|
||||||
|
onSelect: (TypeFilter) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) {
|
||||||
|
val options = listOf(
|
||||||
|
TypeFilter.ALL to "Tous",
|
||||||
|
TypeFilter.GAMES to "Matchs",
|
||||||
|
TypeFilter.PRACTICES to "Entraînements",
|
||||||
|
)
|
||||||
|
options.forEachIndexed { index, (filter, label) ->
|
||||||
|
SegmentedButton(
|
||||||
|
selected = selected == filter,
|
||||||
|
onClick = { onSelect(filter) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index, options.size),
|
||||||
|
) {
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FilterAssistChip(
|
||||||
|
label: String,
|
||||||
|
selectedValue: String?,
|
||||||
|
options: List<String>,
|
||||||
|
onSelect: (String?) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showAllOption: Boolean = true,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val displayValue = selectedValue ?: "Tous"
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
AssistChip(
|
||||||
|
onClick = { expanded = true },
|
||||||
|
label = { Text("$label: $displayValue") },
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowDropDown,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = if (selectedValue != null) {
|
||||||
|
AssistChipDefaults.assistChipColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
labelColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AssistChipDefaults.assistChipColors()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
) {
|
||||||
|
if (showAllOption) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Tous") },
|
||||||
|
onClick = {
|
||||||
|
onSelect(null)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options.forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(option) },
|
||||||
|
onClick = {
|
||||||
|
onSelect(option)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.ui.components
|
package ch.parano.myicek.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
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
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ch.parano.myicek.ui.theme.Dimens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SkeletonCard(modifier: Modifier = Modifier) {
|
||||||
|
val transition = rememberInfiniteTransition(label = "shimmer")
|
||||||
|
val alpha by transition.animateFloat(
|
||||||
|
initialValue = 0.3f,
|
||||||
|
targetValue = 0.6f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(800),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
),
|
||||||
|
label = "shimmerAlpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
val skeletonColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val skeletonModifier = Modifier
|
||||||
|
.alpha(alpha)
|
||||||
|
.background(skeletonColor, RoundedCornerShape(4.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.lg, vertical = Dimens.sm)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.padding(Dimens.lg),
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(Dimens.sm)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.height(20.dp)
|
||||||
|
.width(160.dp)
|
||||||
|
.then(skeletonModifier))
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.then(skeletonModifier))
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.width(200.dp)
|
||||||
|
.then(skeletonModifier))
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.width(180.dp)
|
||||||
|
.then(skeletonModifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.screens
|
||||||
|
|
||||||
|
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
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.filled.ErrorOutline
|
||||||
|
import androidx.compose.material.icons.filled.Event
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Place
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.AssistChipDefaults
|
||||||
|
import androidx.compose.material3.Badge
|
||||||
|
import androidx.compose.material3.BadgedBox
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ch.parano.myicek.ui.PlatformBackHandler
|
||||||
|
import ch.parano.myicek.ui.components.ErrorState
|
||||||
|
import ch.parano.myicek.ui.components.LoadingIndicator
|
||||||
|
import ch.parano.myicek.ui.theme.Dimens
|
||||||
|
import ch.parano.myicek.util.formatEventDate
|
||||||
|
import ch.parano.myicek.util.formatEventTimeRange
|
||||||
|
import ch.parano.myicek.viewmodel.EventDetailViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun EventDetailScreen(
|
||||||
|
gameId: String,
|
||||||
|
account: String,
|
||||||
|
eventTitle: String,
|
||||||
|
eventStart: String,
|
||||||
|
eventEnd: String,
|
||||||
|
viewModel: EventDetailViewModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
PlatformBackHandler { onBack() }
|
||||||
|
|
||||||
|
LaunchedEffect(gameId, account) {
|
||||||
|
viewModel.loadEventDetail(gameId, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(eventTitle) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Retour",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
) { padding ->
|
||||||
|
when {
|
||||||
|
state.isLoading -> {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(padding))
|
||||||
|
}
|
||||||
|
state.error != null -> {
|
||||||
|
ErrorState(
|
||||||
|
icon = Icons.Filled.ErrorOutline,
|
||||||
|
title = "Erreur",
|
||||||
|
message = state.error!!,
|
||||||
|
onRetry = { viewModel.loadEventDetail(gameId, account) },
|
||||||
|
modifier = Modifier.padding(padding),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.eventDetail != null -> {
|
||||||
|
val detail = state.eventDetail!!
|
||||||
|
val players = detail.convocation.available.sortedBy { it.number?.toIntOrNull() ?: Int.MAX_VALUE }
|
||||||
|
val staff = detail.convocation.staff
|
||||||
|
val hasPlayers = players.isNotEmpty()
|
||||||
|
val hasStaff = staff.isNotEmpty()
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(Dimens.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.lg),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(Dimens.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.md),
|
||||||
|
) {
|
||||||
|
InfoRow(Icons.Filled.Event, "Type", detail.type)
|
||||||
|
InfoRow(Icons.Filled.Place, "Lieu", detail.place)
|
||||||
|
InfoRow(Icons.Filled.CalendarMonth, "Date", formatEventDate(eventStart))
|
||||||
|
InfoRow(Icons.Filled.Schedule, "Heure", formatEventTimeRange(eventStart, eventEnd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPlayers && !hasStaff) {
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(Dimens.lg),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Aucun joueur ni personnel convoqué",
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPlayers) {
|
||||||
|
item {
|
||||||
|
BadgedBox(badge = { Badge { Text("${players.size}") } }) {
|
||||||
|
Text(
|
||||||
|
text = "Joueurs",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
players.forEach { player ->
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = player.number?.let { "#$it" } ?: "",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text("${player.fname} ${player.lname}")
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
if (player.dob.isNotBlank()) {
|
||||||
|
Text(player.dob)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (!player.position.isNullOrBlank()) {
|
||||||
|
AssistChip(
|
||||||
|
onClick = {},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
player.position,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = AssistChipDefaults.assistChipColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
labelColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStaff) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Personnel",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
staff.forEach { member ->
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text("${member.fname} ${member.lname}")
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
AssistChip(
|
||||||
|
onClick = {},
|
||||||
|
label = { Text(member.role) },
|
||||||
|
colors = AssistChipDefaults.assistChipColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
labelColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text("Aucune donnée disponible")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoRow(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = label.uppercase(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-5
@@ -15,16 +15,24 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.ui.screens
|
package ch.parano.myicek.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Login
|
||||||
|
import androidx.compose.material.icons.filled.SportsHockey
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
@@ -37,10 +45,12 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ch.parano.myice.viewmodel.AuthViewModel
|
import ch.parano.myicek.ui.theme.Dimens
|
||||||
import ch.parano.myice.viewmodel.AuthState
|
import ch.parano.myicek.viewmodel.AuthState
|
||||||
|
import ch.parano.myicek.viewmodel.AuthViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
@@ -65,22 +75,44 @@ fun LoginScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.SportsHockey,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(Dimens.lg))
|
||||||
Text(
|
Text(
|
||||||
text = "MyIce",
|
text = "MyIce",
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Games",
|
text = "Votre calendrier hockey",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(Modifier.height(Dimens.xxl))
|
||||||
if (state is AuthState.Loading) {
|
if (state is AuthState.Loading) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
} else {
|
} else {
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.login() },
|
onClick = { viewModel.login() },
|
||||||
|
modifier = Modifier.height(48.dp),
|
||||||
) {
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Login,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(Dimens.sm))
|
||||||
Text("Se connecter avec Infomaniak")
|
Text("Se connecter avec Infomaniak")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.CloudOff
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import ch.parano.myicek.filter.TypeFilter
|
||||||
|
import ch.parano.myicek.filter.groupEventsByDate
|
||||||
|
import ch.parano.myicek.ui.components.EmptyState
|
||||||
|
import ch.parano.myicek.ui.components.ErrorState
|
||||||
|
import ch.parano.myicek.ui.components.EventCard
|
||||||
|
import ch.parano.myicek.ui.components.FilterAssistChip
|
||||||
|
import ch.parano.myicek.ui.components.SkeletonCard
|
||||||
|
import ch.parano.myicek.ui.components.TypeFilterSegmented
|
||||||
|
import ch.parano.myicek.ui.theme.Dimens
|
||||||
|
import ch.parano.myicek.util.formatEventDate
|
||||||
|
import ch.parano.myicek.viewmodel.AuthViewModel
|
||||||
|
import ch.parano.myicek.viewmodel.ScheduleViewModel
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ScheduleScreen(
|
||||||
|
scheduleViewModel: ScheduleViewModel,
|
||||||
|
authViewModel: AuthViewModel,
|
||||||
|
onEventClick: (gameId: String, account: String, eventTitle: String, eventStart: String, eventEnd: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheduleState by scheduleViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(scheduleState.selectedAccount) {
|
||||||
|
if (scheduleState.selectedAccount != null && scheduleState.events.isEmpty()) {
|
||||||
|
scheduleViewModel.loadCachedSchedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasActiveFilters = scheduleState.selectedAgegroup != null ||
|
||||||
|
scheduleState.selectedSubgroup != null ||
|
||||||
|
scheduleState.typeFilter != TypeFilter.DEFAULT
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Calendrier") },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { scheduleViewModel.refreshSchedule() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = "Rafraîchir",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
|
scheduleViewModel.clearCache()
|
||||||
|
authViewModel.logout()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||||
|
contentDescription = "Déconnexion",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.lg, vertical = Dimens.sm),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(Dimens.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.sm),
|
||||||
|
) {
|
||||||
|
TypeFilterSegmented(
|
||||||
|
selected = scheduleState.typeFilter,
|
||||||
|
onSelect = { scheduleViewModel.setTypeFilter(it) },
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
FilterAssistChip(
|
||||||
|
label = "Compte",
|
||||||
|
selectedValue = scheduleState.accounts.find { it.name == scheduleState.selectedAccount }?.label,
|
||||||
|
options = scheduleState.accounts.map { it.label },
|
||||||
|
onSelect = { label ->
|
||||||
|
val account = scheduleState.accounts.find { it.label == label }
|
||||||
|
if (account != null) scheduleViewModel.setAccount(account.name)
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
showAllOption = false,
|
||||||
|
)
|
||||||
|
FilterAssistChip(
|
||||||
|
label = "Âge",
|
||||||
|
selectedValue = scheduleState.selectedAgegroup,
|
||||||
|
options = scheduleState.agegroups,
|
||||||
|
onSelect = { scheduleViewModel.setAgegroup(it) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.sm),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
FilterAssistChip(
|
||||||
|
label = "Sous-groupe",
|
||||||
|
selectedValue = scheduleState.selectedSubgroup,
|
||||||
|
options = scheduleState.subgroups,
|
||||||
|
onSelect = { scheduleViewModel.setSubgroup(it) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
if (hasActiveFilters) {
|
||||||
|
AssistChipClear(
|
||||||
|
onClear = {
|
||||||
|
scheduleViewModel.setAgegroup(null)
|
||||||
|
scheduleViewModel.setSubgroup(null)
|
||||||
|
scheduleViewModel.setTypeFilter(TypeFilter.DEFAULT)
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = scheduleState.isRefreshing,
|
||||||
|
onRefresh = { scheduleViewModel.refreshSchedule() },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
scheduleState.isLoading && scheduleState.events.isEmpty() -> {
|
||||||
|
LazyColumn {
|
||||||
|
items(4) { SkeletonCard() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleState.error != null && scheduleState.events.isEmpty() -> {
|
||||||
|
ErrorState(
|
||||||
|
icon = Icons.Filled.CloudOff,
|
||||||
|
title = "Erreur de chargement",
|
||||||
|
message = scheduleState.error!!,
|
||||||
|
onRetry = { scheduleViewModel.refreshSchedule() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scheduleState.filteredEvents.isEmpty() -> {
|
||||||
|
EmptyState(
|
||||||
|
icon = Icons.Filled.CalendarMonth,
|
||||||
|
title = "Aucun événement disponible",
|
||||||
|
message = "Tirez vers le bas pour rafraîchir le calendrier",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val grouped = groupEventsByDate(scheduleState.filteredEvents)
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
grouped.forEach { (dateKey, dayEvents) ->
|
||||||
|
stickyHeader {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.padding(horizontal = Dimens.lg, vertical = Dimens.sm),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = formatEventDate(dateKey + "T00:00:00")
|
||||||
|
.replaceFirstChar { it.uppercase() },
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(dayEvents) { event ->
|
||||||
|
EventCard(
|
||||||
|
event = event,
|
||||||
|
onClick = {
|
||||||
|
onEventClick(
|
||||||
|
event.idEvent,
|
||||||
|
scheduleState.selectedAccount!!,
|
||||||
|
event.title,
|
||||||
|
event.start,
|
||||||
|
event.end,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scheduleState.lastUpdated != null) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Dernière mise à jour: ${formatLastUpdated(scheduleState.lastUpdated!!)}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Dimens.lg),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun AssistChipClear(
|
||||||
|
onClear: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AssistChip(
|
||||||
|
onClick = onClear,
|
||||||
|
label = { Text("Effacer") },
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = "Effacer les filtres",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatLastUpdated(timestamp: Long): String {
|
||||||
|
val instant = Instant.fromEpochMilliseconds(timestamp)
|
||||||
|
val local = instant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
return "${local.date} ${local.time}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
object Dimens {
|
||||||
|
val xs = 4.dp
|
||||||
|
val sm = 8.dp
|
||||||
|
val md = 12.dp
|
||||||
|
val lg = 16.dp
|
||||||
|
val xl = 24.dp
|
||||||
|
val xxl = 32.dp
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Shapes
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
private val LightColors = lightColorScheme(
|
||||||
|
primary = Color(0xFF0D9488),
|
||||||
|
onPrimary = Color(0xFFFFFFFF),
|
||||||
|
primaryContainer = Color(0xFF99F6E4),
|
||||||
|
onPrimaryContainer = Color(0xFF134E4A),
|
||||||
|
secondary = Color(0xFF475569),
|
||||||
|
onSecondary = Color(0xFFFFFFFF),
|
||||||
|
secondaryContainer = Color(0xFFE2E8F0),
|
||||||
|
onSecondaryContainer = Color(0xFF1E293B),
|
||||||
|
tertiary = Color(0xFF0EA5E9),
|
||||||
|
onTertiary = Color(0xFFFFFFFF),
|
||||||
|
tertiaryContainer = Color(0xFFE0F2FE),
|
||||||
|
onTertiaryContainer = Color(0xFF075985),
|
||||||
|
error = Color(0xFFDC2626),
|
||||||
|
onError = Color(0xFFFFFFFF),
|
||||||
|
errorContainer = Color(0xFFFEE2E2),
|
||||||
|
onErrorContainer = Color(0xFF7F1D1D),
|
||||||
|
background = Color(0xFFFAFAFA),
|
||||||
|
onBackground = Color(0xFF1C1917),
|
||||||
|
surface = Color(0xFFFFFFFF),
|
||||||
|
onSurface = Color(0xFF1C1917),
|
||||||
|
surfaceVariant = Color(0xFFF1F5F9),
|
||||||
|
onSurfaceVariant = Color(0xFF475569),
|
||||||
|
outline = Color(0xFFCBD5E1),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DarkColors = darkColorScheme(
|
||||||
|
primary = Color(0xFF2DD4BF),
|
||||||
|
onPrimary = Color(0xFF003833),
|
||||||
|
primaryContainer = Color(0xFF0D9488),
|
||||||
|
onPrimaryContainer = Color(0xFF99F6E4),
|
||||||
|
secondary = Color(0xFF94A3B8),
|
||||||
|
onSecondary = Color(0xFF1E293B),
|
||||||
|
secondaryContainer = Color(0xFF334155),
|
||||||
|
onSecondaryContainer = Color(0xFFE2E8F0),
|
||||||
|
tertiary = Color(0xFF0EA5E9),
|
||||||
|
onTertiary = Color(0xFF003547),
|
||||||
|
background = Color(0xFF0F172A),
|
||||||
|
onBackground = Color(0xFFF1F5F9),
|
||||||
|
surface = Color(0xFF1E293B),
|
||||||
|
onSurface = Color(0xFFF1F5F9),
|
||||||
|
surfaceVariant = Color(0xFF334155),
|
||||||
|
onSurfaceVariant = Color(0xFF94A3B8),
|
||||||
|
outline = Color(0xFF475569),
|
||||||
|
error = Color(0xFFF87171),
|
||||||
|
onError = Color(0xFF7F1D1D),
|
||||||
|
errorContainer = Color(0xFF7F1D1D),
|
||||||
|
onErrorContainer = Color(0xFFFECACA),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val AppTypography = Typography(
|
||||||
|
headlineLarge = TextStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold),
|
||||||
|
titleLarge = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.SemiBold),
|
||||||
|
titleMedium = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.SemiBold),
|
||||||
|
bodyLarge = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.Normal),
|
||||||
|
bodyMedium = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal),
|
||||||
|
bodySmall = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal),
|
||||||
|
labelMedium = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val AppShapes = Shapes(
|
||||||
|
small = RoundedCornerShape(8.dp),
|
||||||
|
medium = RoundedCornerShape(12.dp),
|
||||||
|
large = RoundedCornerShape(16.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MyIceTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = if (darkTheme) DarkColors else LightColors,
|
||||||
|
typography = AppTypography,
|
||||||
|
shapes = AppShapes,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
+30
-6
@@ -15,27 +15,51 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.util
|
package ch.parano.myicek.util
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
|
||||||
|
private val FRENCH_MONTHS = listOf(
|
||||||
|
"janvier", "février", "mars", "avril", "mai", "juin",
|
||||||
|
"juillet", "août", "septembre", "octobre", "novembre", "décembre",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val FRENCH_DAYS = listOf(
|
||||||
|
"Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche",
|
||||||
|
)
|
||||||
|
|
||||||
private fun parseOrNull(iso: String): LocalDateTime? = runCatching {
|
private fun parseOrNull(iso: String): LocalDateTime? = runCatching {
|
||||||
LocalDateTime.parse(iso)
|
LocalDateTime.parse(iso.replace(' ', 'T'))
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
private fun LocalDateTime.dateStr(): String =
|
private fun LocalDateTime.dateStr(): String =
|
||||||
"${day.toString().padStart(2, '0')}.${monthNumber.toString().padStart(2, '0')}.${year.toString().padStart(4, '0')}"
|
"${year.toString().padStart(4, '0')}-${monthNumber.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}"
|
||||||
|
|
||||||
|
private fun LocalDateTime.dateStrLong(): String =
|
||||||
|
"${FRENCH_DAYS[date.dayOfWeek.ordinal]} $day ${FRENCH_MONTHS[monthNumber - 1]} $year"
|
||||||
|
|
||||||
private fun LocalDateTime.timeStr(): String =
|
private fun LocalDateTime.timeStr(): String =
|
||||||
"${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}"
|
if (minute == 0) "${hour}h" else "${hour}h${minute.toString().padStart(2, '0')}"
|
||||||
|
|
||||||
fun formatEventDateTime(start: String, end: String): String {
|
fun formatEventDateTime(start: String, end: String): String {
|
||||||
val startDt = parseOrNull(start)
|
val startDt = parseOrNull(start)
|
||||||
val endDt = parseOrNull(end)
|
val endDt = parseOrNull(end)
|
||||||
if (startDt == null || endDt == null) return "$start - $end"
|
if (startDt == null || endDt == null) return "$start - $end"
|
||||||
return if (startDt.date == endDt.date) {
|
return if (startDt.date == endDt.date) {
|
||||||
"${startDt.dateStr()} ${startDt.timeStr()} - ${endDt.timeStr()}"
|
"${startDt.dateStrLong()} de ${startDt.timeStr()} à ${endDt.timeStr()}"
|
||||||
} else {
|
} else {
|
||||||
"${startDt.dateStr()} ${startDt.timeStr()} - ${endDt.timeStr()} (${endDt.dateStr()})"
|
"${startDt.dateStrLong()} de ${startDt.timeStr()} à ${endDt.timeStr()} (${endDt.dateStrLong()})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun formatEventDate(start: String): String {
|
||||||
|
val dt = parseOrNull(start)
|
||||||
|
return dt?.dateStrLong() ?: start
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatEventTimeRange(start: String, end: String): String {
|
||||||
|
val startDt = parseOrNull(start)
|
||||||
|
val endDt = parseOrNull(end)
|
||||||
|
if (startDt == null || endDt == null) return "$start - $end"
|
||||||
|
return "${startDt.timeStr()} - ${endDt.timeStr()}"
|
||||||
|
}
|
||||||
+6
-6
@@ -15,13 +15,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.viewmodel
|
package ch.parano.myicek.viewmodel
|
||||||
|
|
||||||
import ch.parano.myice.auth.AuthSettings
|
import ch.parano.myicek.auth.AuthSettings
|
||||||
import ch.parano.myice.auth.OAuthClient
|
import ch.parano.myicek.auth.OAuthClient
|
||||||
import ch.parano.myice.network.ApiConfig
|
import ch.parano.myicek.network.ApiConfig
|
||||||
import ch.parano.myice.network.ApiService
|
import ch.parano.myicek.network.ApiService
|
||||||
import ch.parano.myice.network.AuthTokenHolder
|
import ch.parano.myicek.network.AuthTokenHolder
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
+3
-3
@@ -15,10 +15,10 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.viewmodel
|
package ch.parano.myicek.viewmodel
|
||||||
|
|
||||||
import ch.parano.myice.models.EventDetail
|
import ch.parano.myicek.models.EventDetail
|
||||||
import ch.parano.myice.network.ApiService
|
import ch.parano.myicek.network.ApiService
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
+10
-10
@@ -15,17 +15,17 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.viewmodel
|
package ch.parano.myicek.viewmodel
|
||||||
|
|
||||||
import ch.parano.myice.auth.AuthSettings
|
import ch.parano.myicek.auth.AuthSettings
|
||||||
import ch.parano.myice.cache.ScheduleCache
|
import ch.parano.myicek.cache.ScheduleCache
|
||||||
import ch.parano.myice.filter.TypeFilter
|
import ch.parano.myicek.filter.TypeFilter
|
||||||
import ch.parano.myice.filter.distinctAgegroups
|
import ch.parano.myicek.filter.distinctAgegroups
|
||||||
import ch.parano.myice.filter.distinctSubgroups
|
import ch.parano.myicek.filter.distinctSubgroups
|
||||||
import ch.parano.myice.filter.filterEvents
|
import ch.parano.myicek.filter.filterEvents
|
||||||
import ch.parano.myice.models.Account
|
import ch.parano.myicek.models.Account
|
||||||
import ch.parano.myice.models.Event
|
import ch.parano.myicek.models.Event
|
||||||
import ch.parano.myice.network.ApiService
|
import ch.parano.myicek.network.ApiService
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek
|
||||||
|
|
||||||
|
import com.russhwolf.settings.MapSettings
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class WindowStateStorageTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndLoadWindowState() {
|
||||||
|
val settings = MapSettings()
|
||||||
|
val storage = WindowStateStorage(settings)
|
||||||
|
|
||||||
|
storage.save(width = 1200, height = 800, x = 100, y = 50)
|
||||||
|
|
||||||
|
val state = storage.load()
|
||||||
|
assertEquals(1200, state?.width)
|
||||||
|
assertEquals(800, state?.height)
|
||||||
|
assertEquals(100, state?.x)
|
||||||
|
assertEquals(50, state?.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadReturnsNullWhenNothingSaved() {
|
||||||
|
val settings = MapSettings()
|
||||||
|
val storage = WindowStateStorage(settings)
|
||||||
|
|
||||||
|
assertNull(storage.load())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveOverwritesPreviousState() {
|
||||||
|
val settings = MapSettings()
|
||||||
|
val storage = WindowStateStorage(settings)
|
||||||
|
|
||||||
|
storage.save(width = 1200, height = 800, x = 100, y = 50)
|
||||||
|
storage.save(width = 1024, height = 768, x = 200, y = 75)
|
||||||
|
|
||||||
|
val state = storage.load()
|
||||||
|
assertEquals(1024, state?.width)
|
||||||
|
assertEquals(768, state?.height)
|
||||||
|
assertEquals(200, state?.x)
|
||||||
|
assertEquals(75, state?.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -15,9 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.cache
|
package ch.parano.myicek.cache
|
||||||
|
|
||||||
import ch.parano.myice.models.Event
|
import ch.parano.myicek.models.Event
|
||||||
import com.russhwolf.settings.MapSettings
|
import com.russhwolf.settings.MapSettings
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
+48
-3
@@ -15,11 +15,12 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.filter
|
package ch.parano.myicek.filter
|
||||||
|
|
||||||
import ch.parano.myice.models.Event
|
import ch.parano.myicek.models.Event
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class EventFilterTest {
|
class EventFilterTest {
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ class EventFilterTest {
|
|||||||
agegroup: String = "U13 (Elite)",
|
agegroup: String = "U13 (Elite)",
|
||||||
name: String = "U13 Elite - HC Ajoie",
|
name: String = "U13 Elite - HC Ajoie",
|
||||||
eventType: String = "Jeu",
|
eventType: String = "Jeu",
|
||||||
|
start: String = "2024-11-10T14:00:00",
|
||||||
) = Event(
|
) = Event(
|
||||||
idEvent = id,
|
idEvent = id,
|
||||||
agegroup = agegroup,
|
agegroup = agegroup,
|
||||||
@@ -35,7 +37,7 @@ class EventFilterTest {
|
|||||||
title = "Title",
|
title = "Title",
|
||||||
opponent = "Opp",
|
opponent = "Opp",
|
||||||
place = "Place",
|
place = "Place",
|
||||||
start = "2024-11-10T14:00:00",
|
start = start,
|
||||||
end = "2024-11-10T16:15:00",
|
end = "2024-11-10T16:15:00",
|
||||||
color = "#e4222e",
|
color = "#e4222e",
|
||||||
eventType = eventType,
|
eventType = eventType,
|
||||||
@@ -171,4 +173,47 @@ class EventFilterTest {
|
|||||||
assertEquals("U13 Elite", result[0])
|
assertEquals("U13 Elite", result[0])
|
||||||
assertEquals("U13 Top", result[1])
|
assertEquals("U13 Top", result[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGroupEventsByDateSingleDay() {
|
||||||
|
val events = listOf(
|
||||||
|
event(id = "1", start = "2024-11-10T14:00:00"),
|
||||||
|
event(id = "2", start = "2024-11-10T16:00:00"),
|
||||||
|
)
|
||||||
|
val grouped = groupEventsByDate(events)
|
||||||
|
assertEquals(1, grouped.size)
|
||||||
|
assertEquals("2024-11-10", grouped[0].first)
|
||||||
|
assertEquals(2, grouped[0].second.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGroupEventsByDateMultipleDays() {
|
||||||
|
val events = listOf(
|
||||||
|
event(id = "1", start = "2024-11-11T10:00:00"),
|
||||||
|
event(id = "2", start = "2024-11-10T14:00:00"),
|
||||||
|
event(id = "3", start = "2024-11-10T16:00:00"),
|
||||||
|
)
|
||||||
|
val grouped = groupEventsByDate(events)
|
||||||
|
assertEquals(2, grouped.size)
|
||||||
|
assertEquals("2024-11-10", grouped[0].first)
|
||||||
|
assertEquals(2, grouped[0].second.size)
|
||||||
|
assertEquals("2024-11-11", grouped[1].first)
|
||||||
|
assertEquals(1, grouped[1].second.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGroupEventsByDateWithSpaceSeparator() {
|
||||||
|
val events = listOf(
|
||||||
|
event(id = "1", start = "2024-11-10 14:00:00"),
|
||||||
|
)
|
||||||
|
val grouped = groupEventsByDate(events)
|
||||||
|
assertEquals(1, grouped.size)
|
||||||
|
assertEquals("2024-11-10", grouped[0].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGroupEventsByDateEmpty() {
|
||||||
|
val grouped = groupEventsByDate(emptyList())
|
||||||
|
assertTrue(grouped.isEmpty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.models
|
package ch.parano.myicek.models
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
+2
-2
@@ -15,9 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
import ch.parano.myice.models.EventDetail
|
import ch.parano.myicek.models.EventDetail
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.mock.MockEngine
|
import io.ktor.client.engine.mock.MockEngine
|
||||||
import io.ktor.client.engine.mock.respond
|
import io.ktor.client.engine.mock.respond
|
||||||
+18
-3
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.util
|
package ch.parano.myicek.util
|
||||||
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@@ -28,7 +28,16 @@ class EventDateTimeFormatterTest {
|
|||||||
start = "2024-11-10T14:00:00",
|
start = "2024-11-10T14:00:00",
|
||||||
end = "2024-11-10T16:15:00",
|
end = "2024-11-10T16:15:00",
|
||||||
)
|
)
|
||||||
assertEquals("10.11.2024 14:00 - 16:15", formatted)
|
assertEquals("Dimanche 10 novembre 2024 de 14h à 16h15", formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun formatsSameDayEventWithSpaceSeparator() {
|
||||||
|
val formatted = formatEventDateTime(
|
||||||
|
start = "2026-08-09 10:00:00",
|
||||||
|
end = "2026-08-09 12:00:00",
|
||||||
|
)
|
||||||
|
assertEquals("Dimanche 9 août 2026 de 10h à 12h", formatted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -37,7 +46,7 @@ class EventDateTimeFormatterTest {
|
|||||||
start = "2024-11-10T20:00:00",
|
start = "2024-11-10T20:00:00",
|
||||||
end = "2024-11-11T01:30:00",
|
end = "2024-11-11T01:30:00",
|
||||||
)
|
)
|
||||||
assertEquals("10.11.2024 20:00 - 01:30 (11.11.2024)", formatted)
|
assertEquals("Dimanche 10 novembre 2024 de 20h à 1h30 (Lundi 11 novembre 2024)", formatted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -57,4 +66,10 @@ class EventDateTimeFormatterTest {
|
|||||||
val formatted = formatEventDateTime(start = "s", end = "e")
|
val formatted = formatEventDateTime(start = "s", end = "e")
|
||||||
assertEquals("s - e", formatted)
|
assertEquals("s - e", formatted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun formatsEventDateWithDayOfWeek() {
|
||||||
|
val formatted = formatEventDate("2026-08-09T10:00:00")
|
||||||
|
assertEquals("Dimanche 9 août 2026", formatted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+6
-6
@@ -15,13 +15,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.viewmodel
|
package ch.parano.myicek.viewmodel
|
||||||
|
|
||||||
import ch.parano.myice.cache.ScheduleCache
|
import ch.parano.myicek.cache.ScheduleCache
|
||||||
import ch.parano.myice.filter.TypeFilter
|
import ch.parano.myicek.filter.TypeFilter
|
||||||
import ch.parano.myice.models.Account
|
import ch.parano.myicek.models.Account
|
||||||
import ch.parano.myice.models.Event
|
import ch.parano.myicek.models.Event
|
||||||
import ch.parano.myice.network.ApiService
|
import ch.parano.myicek.network.ApiService
|
||||||
import com.russhwolf.settings.MapSettings
|
import com.russhwolf.settings.MapSettings
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.mock.MockEngine
|
import io.ktor.client.engine.mock.MockEngine
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek
|
||||||
|
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.WindowPosition
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import ch.parano.myicek.auth.DesktopOAuthClient
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
|
import java.util.prefs.Preferences
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
val storage = WindowStateStorage(Settings())
|
||||||
|
val saved = storage.load()
|
||||||
|
val windowState = rememberWindowState(
|
||||||
|
width = saved?.width?.dp ?: 1024.dp,
|
||||||
|
height = saved?.height?.dp ?: 768.dp,
|
||||||
|
position = if (saved != null) WindowPosition(saved.x.dp, saved.y.dp) else WindowPosition(0.dp, 0.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Window(
|
||||||
|
onCloseRequest = ::exitApplication,
|
||||||
|
title = "MyIce",
|
||||||
|
state = windowState,
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { windowState.size to windowState.position }
|
||||||
|
.collect { (size, position) ->
|
||||||
|
storage.save(
|
||||||
|
width = size.width.value.toInt(),
|
||||||
|
height = size.height.value.toInt(),
|
||||||
|
x = position.x.value.toInt(),
|
||||||
|
y = position.y.value.toInt(),
|
||||||
|
)
|
||||||
|
Preferences.userRoot().flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
App(oauthClient = DesktopOAuthClient())
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.auth
|
package ch.parano.myicek.auth
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
import io.ktor.client.engine.HttpClientEngineConfig
|
import io.ktor.client.engine.HttpClientEngineConfig
|
||||||
import io.ktor.client.engine.HttpClientEngineFactory
|
import io.ktor.client.engine.HttpClientEngineFactory
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit) {
|
||||||
|
}
|
||||||
+2
-2
@@ -15,10 +15,10 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice
|
package ch.parano.myicek
|
||||||
|
|
||||||
import androidx.compose.ui.window.ComposeUIViewController
|
import androidx.compose.ui.window.ComposeUIViewController
|
||||||
import ch.parano.myice.auth.IosOAuthClient
|
import ch.parano.myicek.auth.IosOAuthClient
|
||||||
|
|
||||||
fun MainViewController() = ComposeUIViewController {
|
fun MainViewController() = ComposeUIViewController {
|
||||||
App(oauthClient = IosOAuthClient())
|
App(oauthClient = IosOAuthClient())
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.auth
|
package ch.parano.myicek.auth
|
||||||
|
|
||||||
import kotlinx.cinterop.ExperimentalForeignApi
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package ch.parano.myice.network
|
package ch.parano.myicek.network
|
||||||
|
|
||||||
import io.ktor.client.engine.HttpClientEngineConfig
|
import io.ktor.client.engine.HttpClientEngineConfig
|
||||||
import io.ktor.client.engine.HttpClientEngineFactory
|
import io.ktor.client.engine.HttpClientEngineFactory
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
||||||
|
// Copyright (C) 2026 parano.ch
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ch.parano.myicek.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit) {
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
The existing [MyIce webapp](https://gitea.parano.ch/herel/myice) is a FastAPI backend at `https://myice.parano.ch` that proxies the upstream MyIce Hockey API (`app.myice.hockey`). It serves a single-page web frontend (`index.html`) and a JSON API consumed by a Flutter mobile app (`myice_mobile/`).
|
The existing [MyIce webapp](https://gitea.parano.ch/herel/myice) is a FastAPI backend at `https://myice.parano.ch` that proxies the upstream MyIce Hockey API (`app.myice.hockey`). It serves a single-page web frontend (`index.html`) and a JSON API.
|
||||||
|
|
||||||
This spec describes a **Kotlin Multiplatform (KMP) port** of the Flutter app, targeting **Android, iOS, and Desktop** (macOS/Linux/Windows JVM). It mirrors the Flutter app's clean, simple architecture while using idiomatic KMP tooling, and adds one small feature improvement: showing **practices** alongside games (the Flutter app fetches but discards them).
|
This spec describes a **Kotlin Multiplatform (KMP) port** of the Flutter app, targeting **Android, iOS, and Desktop** (macOS/Linux/Windows JVM). It mirrors the Flutter app's clean, simple architecture while using idiomatic KMP tooling, and adds one small feature improvement: showing **practices** alongside games (the Flutter app fetches but discards them).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# MyIce UI Redesign — Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-08-06
|
||||||
|
**Status:** Approved (pending review)
|
||||||
|
**Scope:** Refonte visuelle complète — thème, 4 écrans, composants
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
L'app MyIce (KMP, Compose Multiplatform) est fonctionnellement complète mais visuellement squelettique: thème à 4 slots Material 3, zéro icônerie, listes plates, couleurs hardcodées, accents français manquants, pas de regroupement par date, pas de typographie dédiée.
|
||||||
|
|
||||||
|
Ce spec décrit une refonte visuelle complète dans une direction **moderne & épurée** avec un accent **teal arctique**.
|
||||||
|
|
||||||
|
## Direction visuelle
|
||||||
|
|
||||||
|
- **Style:** Moderne & épuré — beaucoup d'air, surfaces propres, typo hiérarchisée, accents subtils
|
||||||
|
- **Accent:** Teal arctique (`#0D9488` light / `#2DD4BF` dark) — évoque la glace et le hockey
|
||||||
|
- **Philosophie:** Le chrome disparaît, les données sont les vedettes
|
||||||
|
|
||||||
|
## 1. Thème & Design Tokens
|
||||||
|
|
||||||
|
### 1.1 Palette (Material 3 complet)
|
||||||
|
|
||||||
|
**Light scheme:**
|
||||||
|
|
||||||
|
| Slot | Valeur | Note |
|
||||||
|
|------|--------|------|
|
||||||
|
| primary | `#0D9488` (teal-600) | |
|
||||||
|
| onPrimary | `#FFFFFF` | |
|
||||||
|
| primaryContainer | `#99F6E4` (teal-100) | |
|
||||||
|
| onPrimaryContainer | `#134E4A` (teal-900) | |
|
||||||
|
| secondary | `#475569` (slate-600) | |
|
||||||
|
| secondaryContainer | `#E2E8F0` (slate-200) | |
|
||||||
|
| onSecondaryContainer | `#1E293B` | |
|
||||||
|
| tertiary | `#0EA5E9` (sky-500) | états interactifs secondaires |
|
||||||
|
| tertiaryContainer | `#E0F2FE` | |
|
||||||
|
| error | `#DC2626` | |
|
||||||
|
| onError | `#FFFFFF` | |
|
||||||
|
| errorContainer | `#FEE2E2` | |
|
||||||
|
| onErrorContainer | `#7F1D1D` | |
|
||||||
|
| background | `#FAFAFA` (warm white) | |
|
||||||
|
| onBackground | `#1C1917` | |
|
||||||
|
| surface | `#FFFFFF` | |
|
||||||
|
| onSurface | `#1C1917` (warm dark) | |
|
||||||
|
| surfaceVariant | `#F1F5F9` (slate-100) | |
|
||||||
|
| onSurfaceVariant | `#475569` | |
|
||||||
|
| outline | `#CBD5E1` (slate-300) | |
|
||||||
|
|
||||||
|
**Dark scheme:**
|
||||||
|
|
||||||
|
| Slot | Valeur | Note |
|
||||||
|
|------|--------|------|
|
||||||
|
| primary | `#2DD4BF` (teal-400) | |
|
||||||
|
| onPrimary | `#003833` | |
|
||||||
|
| primaryContainer | `#0D9488` | |
|
||||||
|
| onPrimaryContainer | `#99F6E4` | |
|
||||||
|
| secondary | `#94A3B8` (slate-400) | |
|
||||||
|
| secondaryContainer | `#334155` (slate-700) | |
|
||||||
|
| tertiary | `#0EA5E9` | |
|
||||||
|
| background | `#0F172A` (slate-900) | |
|
||||||
|
| onBackground | `#F1F5F9` | |
|
||||||
|
| surface | `#1E293B` (slate-800) | |
|
||||||
|
| onSurface | `#F1F5F9` | |
|
||||||
|
| surfaceVariant | `#334155` | |
|
||||||
|
| onSurfaceVariant | `#94A3B8` | |
|
||||||
|
| outline | `#475569` | |
|
||||||
|
| error | `#F87171` | |
|
||||||
|
| onError | `#7F1D1D` | |
|
||||||
|
| errorContainer | `#7F1D1D` | |
|
||||||
|
| onErrorContainer | `#FECACA` | |
|
||||||
|
|
||||||
|
### 1.2 Typographie
|
||||||
|
|
||||||
|
Échelle système (pas de custom font — trop complexe en KMP), mais poids et tailles définis explicitement via `Typography()`:
|
||||||
|
|
||||||
|
| Style | Taille | Poids | Usage |
|
||||||
|
|-------|--------|-------|-------|
|
||||||
|
| headlineLarge | 28sp | Bold | App name (login) |
|
||||||
|
| titleLarge | 22sp | SemiBold | Screen titles (TopAppBar) |
|
||||||
|
| titleMedium | 16sp | SemiBold | Card titles |
|
||||||
|
| bodyLarge | 15sp | Regular | Main content |
|
||||||
|
| bodyMedium | 14sp | Regular | Secondary info |
|
||||||
|
| bodySmall | 13sp | Regular | Metadata, labels |
|
||||||
|
| labelMedium | 12sp | Medium | Chips, badges, section headers |
|
||||||
|
|
||||||
|
### 1.3 Shapes
|
||||||
|
|
||||||
|
| Nom | Rayon | Usage |
|
||||||
|
|-----|-------|-------|
|
||||||
|
| small | 8dp | Chips, badges |
|
||||||
|
| medium | 12dp | Cards, inputs |
|
||||||
|
| large | 16dp | Sheets, big cards |
|
||||||
|
|
||||||
|
### 1.4 Spacing tokens (`Dimens` object)
|
||||||
|
|
||||||
|
| Token | Valeur |
|
||||||
|
|-------|--------|
|
||||||
|
| xs | 4dp |
|
||||||
|
| sm | 8dp |
|
||||||
|
| md | 12dp |
|
||||||
|
| lg | 16dp |
|
||||||
|
| xl | 24dp |
|
||||||
|
| xxl | 32dp |
|
||||||
|
|
||||||
|
### 1.5 Elevation
|
||||||
|
|
||||||
|
| Niveau | Valeur | Usage |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| level0 | 0dp | Flat surfaces |
|
||||||
|
| level1 | 1dp | Cards |
|
||||||
|
| level2 | 3dp | App bar, dialogs |
|
||||||
|
|
||||||
|
## 2. Écrans
|
||||||
|
|
||||||
|
### 2.1 LoginScreen
|
||||||
|
|
||||||
|
Layout centré vertical:
|
||||||
|
- Icône `SportsHockey` (Material) 48dp dans un cercle `primaryContainer` 80dp, en haut
|
||||||
|
- `"MyIce"` en `headlineLarge` (le "K" de "MyIceK" était un codename tech — supprimé)
|
||||||
|
- Sous-titre `"Votre calendrier hockey"` en `titleMedium` `onSurfaceVariant`
|
||||||
|
- Spacer `xxl` (32dp)
|
||||||
|
- `Button` plein, 48dp de haut, icône `Login` leading: `"Se connecter avec Infomaniak"`
|
||||||
|
- Snackbar erreur en `errorContainer`/`onErrorContainer`
|
||||||
|
- Fond `background`, contenu centré verticalement (pas collé au haut)
|
||||||
|
|
||||||
|
### 2.2 ScheduleScreen
|
||||||
|
|
||||||
|
**TopAppBar:**
|
||||||
|
- Titre `"Calendrier"` (au lieu de "MyIceK")
|
||||||
|
- Actions: icône `Refresh` + icône `Logout` uniquement (Material icons, pas de texte)
|
||||||
|
- L'email utilisateur est supprimé de la barre (trop encombrant sur mobile). Il n'est pas relocalisé — un menu compte futur est hors scope.
|
||||||
|
|
||||||
|
**Filtres:**
|
||||||
|
- `SegmentedButton` (singleChoice, 3 segments) sur une ligne: `"Tous"` / `"Matchs"` / `"Entraînements"` (remplace le `FilterDropdown` pour le type)
|
||||||
|
- `AssistChip` cliquables pour Compte / Age / Sous-groupe (affichent la valeur sélectionnée, `"Tous"` par défaut). Utilisent `account.label` au lieu de `account.name` pour le dropdown Compte.
|
||||||
|
- `AssistChip` "Effacer" avec icône `Close` apparaît si au moins un filtre est actif
|
||||||
|
- Section filtres dans une `Card` `surfaceVariant`, `medium` shape, padding `lg`
|
||||||
|
|
||||||
|
**Liste d'événements:**
|
||||||
|
- `LazyColumn` avec **sticky headers** par jour: `"Samedi 15 mars"` en `labelMedium` uppercase `onSurfaceVariant`, fond `surface`, divider subtil en dessous
|
||||||
|
- `EventCard` repensé (voir Section 3)
|
||||||
|
- Indicateur `"Dernière mise à jour: ..."` en bas de liste, `bodySmall` `onSurfaceVariant` (utilise `lastUpdated` du state — actuellement non affiché)
|
||||||
|
- `PullToRefreshBox` conservé (Material 3 par défaut)
|
||||||
|
|
||||||
|
**États partagés:**
|
||||||
|
- Loading: 3-4 `SkeletonCard` animés (voir Section 3)
|
||||||
|
- Error: `ErrorState` avec icône `CloudOff`, message, bouton `"Réessayer"`
|
||||||
|
- Empty: `EmptyState` avec icône `CalendarClear`, `"Aucun événement disponible"`
|
||||||
|
|
||||||
|
### 2.3 EventDetailScreen
|
||||||
|
|
||||||
|
**TopAppBar:**
|
||||||
|
- Navigation icon: `ArrowBack` (icône, pas `TextButton("Retour")`)
|
||||||
|
- Titre: titre de l'événement tronqué
|
||||||
|
- `PlatformBackHandler` conservé
|
||||||
|
|
||||||
|
**Section infos (carte unique, `medium` shape, elevation 1dp):**
|
||||||
|
- Chaque ligne: icône leading 18dp `onSurfaceVariant` + label `bodySmall` uppercase `onSurfaceVariant` + valeur `bodyLarge` `onSurface`
|
||||||
|
- Lignes: `Event` → type, `Place` → lieu, `CalendarMonth` → date, `Schedule` → heure
|
||||||
|
- Plus de `"Type: Match"` interpolé — label et valeur visuellement distincts
|
||||||
|
|
||||||
|
**Convocation:**
|
||||||
|
- En-tête `"Joueurs (N)"` en `titleMedium` SemiBold + `BadgedBox` avec count
|
||||||
|
- **Joueurs:** `ListItem` Material 3 — numéro dans un cercle `primaryContainer` 40dp (avatar), nom `bodyLarge`, position + `"(née 2008)"` en `bodySmall` dessous. Triés par numéro.
|
||||||
|
- Si aucun joueur: carte `errorContainer` avec icône `Warning` + `"Aucun joueur ni personnel convoqué"`
|
||||||
|
- **Personnel:** `ListItem` avec icône `Person` leading, nom `bodyLarge`, role en `AssistChip` `secondaryContainer` à droite
|
||||||
|
- En-tête `"Personnel"` en `titleMedium` SemiBold
|
||||||
|
|
||||||
|
**États partagés:**
|
||||||
|
- Loading: `LoadingIndicator` conservé
|
||||||
|
- Error: `ErrorState` avec icône `ErrorOutline`, message, bouton `"Réessayer"`
|
||||||
|
|
||||||
|
### 2.4 Navigation & transitions
|
||||||
|
|
||||||
|
- Navigation manuelle conservée (sealed class + `mutableStateOf` — pas de lib externe)
|
||||||
|
- **`AnimatedContent`** ajouté dans `App.kt` pour transitions fondues entre écrans (`tween(300ms)`)
|
||||||
|
- `PlatformBackHandler` conservé (known limitation: no-op sur desktop/iOS)
|
||||||
|
|
||||||
|
## 3. Composants
|
||||||
|
|
||||||
|
### 3.1 EventCard (repensé)
|
||||||
|
|
||||||
|
Carte `medium` shape (12dp), elevation 1dp, cliquable:
|
||||||
|
- **Barre couleur** 4dp à gauche via `Modifier.border` sur le `Card` entier (pas un `Row`) — couleur `event.color` (hex parsé), fallback `primary`
|
||||||
|
- **Badge type** en haut-droite: `AssistChip` `"Match"` (couleur teal/primary) ou `"Entraînement"` (couleur secondary/slate)
|
||||||
|
- **Titre:** pour un match (`eventType == "Jeu"`): `"Match vs {opponent}"`. Pour un entraînement: `"Entraînement"`. Suppression de la ligne `"Adversaire:"` pour les entraînements.
|
||||||
|
- **Lignes info** avec icônes leading 18dp `onSurfaceVariant`:
|
||||||
|
- `Place` 18dp + lieu en `bodyMedium`
|
||||||
|
- `Schedule` 18dp + heure condensée (`formatEventTimeRange`: `"18h00 – 19h30"`)
|
||||||
|
- **Âge + nom** (`"{agegroup} - {name}"`) en `titleMedium` SemiBold
|
||||||
|
- La date est dans le header de section (pas dans la card)
|
||||||
|
|
||||||
|
### 3.2 SkeletonCard
|
||||||
|
|
||||||
|
Carte placeholder grise aux mêmes proportions que `EventCard`:
|
||||||
|
- Fond `surfaceVariant`, `alpha(0.3f)`
|
||||||
|
- Animation shimmer: translation horizontale d'un dégradé clair (`InfiniteTransition` + `rememberInfiniteTransition`)
|
||||||
|
- 4 lignes fictives (titre + 2 lignes info + badge)
|
||||||
|
- Réutilisé pour le loading state du Schedule
|
||||||
|
|
||||||
|
### 3.3 EmptyState / ErrorState (composables partagés)
|
||||||
|
|
||||||
|
```
|
||||||
|
EmptyState(icon: ImageVector, title: String, message: String)
|
||||||
|
ErrorState(icon: ImageVector, title: String, message: String, onRetry: () -> Unit)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Centrés verticalement dans un `Column` (`Arrangement.Center`, `HorizontalAlignment.CenterHorizontally`)
|
||||||
|
- Icône 48dp `onSurfaceVariant`
|
||||||
|
- Titre `titleMedium` `onSurface`
|
||||||
|
- Message `bodyMedium` `onSurfaceVariant`
|
||||||
|
- `ErrorState` ajoute un `Button` `"Réessayer"` avec `onRetry`
|
||||||
|
- Réutilisés sur ScheduleScreen et EventDetailScreen
|
||||||
|
|
||||||
|
### 3.4 Wrappers filtres
|
||||||
|
|
||||||
|
- `SegmentedButton` (singleChoice) pour le type: 3 options `"Tous"/"Matchs"/"Entraînements"`, mappe vers `TypeFilter.ALL/GAMES/PRACTICES`
|
||||||
|
- `AssistChip` wrapper pour Compte/Age/Sous-groupe: affiche label + valeur sélectionnée, clic ouvre un `DropdownMenu`
|
||||||
|
- `AssistChip` "Effacer" avec icône `Close`: visible si au moins un filtre non-défaut est actif, reset tous les filtres
|
||||||
|
|
||||||
|
## 4. Corrections transversales
|
||||||
|
|
||||||
|
### 4.1 Accents français
|
||||||
|
|
||||||
|
Toutes les strings UI sont corrigées:
|
||||||
|
- "Rafraichir" → "Rafraîchir"
|
||||||
|
- "Deconnexion" → "Déconnexion"
|
||||||
|
- "Reessayer" → "Réessayer"
|
||||||
|
- "Aucun evenement" → "Aucun événement"
|
||||||
|
- "Entrainements" → "Entraînements"
|
||||||
|
- "convoque" → "convoqué"
|
||||||
|
- "donnees" → "données"
|
||||||
|
|
||||||
|
### 4.2 Couleurs hardcodées supprimées
|
||||||
|
|
||||||
|
- `Color.Gray` (ScheduleScreen) → `onSurfaceVariant`
|
||||||
|
- `Color(0xFFFFC107)` (EventDetailScreen warning) → `errorContainer`/`onErrorContainer`
|
||||||
|
- `Color(0xFF000000 or value)` (EventCard color mask) → `event.color` direct ou `primary` fallback via une util helper `parseHexColor`
|
||||||
|
|
||||||
|
### 4.3 Branding
|
||||||
|
|
||||||
|
- "MyIceK" → "MyIce" partout (TopAppBar, LoginScreen, titre fenêtre desktop)
|
||||||
|
- `account.label` utilisé au lieu de `account.name` dans le dropdown Compte
|
||||||
|
|
||||||
|
### 4.4 Strings
|
||||||
|
|
||||||
|
Toutes les strings restent hardcodées inline (pas de localization infrastructure — hors scope). Pas de `object Strings` non plus: ajouter une couche d'indirection sans localization réelle est du YAGNI. Les accents français (4.1) sont corrigés directement dans les strings inline.
|
||||||
|
|
||||||
|
## 5. Hors scope
|
||||||
|
|
||||||
|
- Migration vers Navigation Compose library
|
||||||
|
- `viewModel()` au lieu de `remember{}` (known limitation — perte de state rotation Android)
|
||||||
|
- Localization infrastructure (string resources)
|
||||||
|
- Custom fonts (trop complexe en KMP)
|
||||||
|
- Tests UI (Composition Testing) — le projet n'a pas de framework de test UI configuré
|
||||||
|
- iOS `.xcodeproj` creation
|
||||||
|
- Desktop window icon / menu bar
|
||||||
|
|
||||||
|
## 6. Fichiers impactés
|
||||||
|
|
||||||
|
| Fichier | Changement |
|
||||||
|
|---------|------------|
|
||||||
|
| `ui/theme/Theme.kt` | Réécriture complète: palette, typo, shapes, elevation, `Dimens` |
|
||||||
|
| `ui/theme/Color.kt` | Nouveau — définitions de couleur teal/slate |
|
||||||
|
| `ui/theme/Dimens.kt` | Nouveau — spacing tokens |
|
||||||
|
| `ui/screens/LoginScreen.kt` | Refonte: icône, branding, layout |
|
||||||
|
| `ui/screens/ScheduleScreen.kt` | Refonte: TopAppBar, chips filtres, sticky headers, états partagés, `lastUpdated` |
|
||||||
|
| `ui/screens/EventDetailScreen.kt` | Refonte: icône back, section infos stylisée, ListItems pour joueurs/staff |
|
||||||
|
| `ui/components/EventCard.kt` | Refonte: border couleur, badge type, icônes leading, logique match/entraînement |
|
||||||
|
| `ui/components/SkeletonCard.kt` | Nouveau — skeleton loading |
|
||||||
|
| `ui/components/EmptyState.kt` | Nouveau — état vide partagé |
|
||||||
|
| `ui/components/ErrorState.kt` | Nouveau — état erreur partagé |
|
||||||
|
| `ui/components/FilterChips.kt` | Nouveau — SegmentedButton + AssistChips (remplace FilterDropdown) |
|
||||||
|
| `ui/components/FilterDropdown.kt` | Supprimé — remplacé par FilterChips |
|
||||||
|
| `ui/components/LoadingIndicator.kt` | Conservé (utilisé par EventDetail) |
|
||||||
|
| `App.kt` | `AnimatedContent` pour transitions, branding "MyIce" |
|
||||||
|
| `util/EventDateTimeFormatter.kt` | Ajout `formatEventTimeRange` condensé (existant conservé) |
|
||||||
|
| `desktopMain/Main.kt` | Titre fenêtre "MyIce" |
|
||||||
@@ -2,3 +2,4 @@ kotlin.code.style=official
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
org.gradle.jvmargs=-Xmx4096m
|
org.gradle.jvmargs=-Xmx4096m
|
||||||
|
compose.desktop.packaging.checkJdkVendor=false
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>ch.parano.myice</string>
|
<string>ch.parano.myicek</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>myice</string>
|
<string>myice</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user