Compare commits

..
9 changed files with 59 additions and 16 deletions
-1
View File
@@ -3,7 +3,6 @@
## Project
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
+1 -1
View File
@@ -1,7 +1,7 @@
# MyIce Kotlin Multiplatform
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
@@ -42,7 +42,13 @@ import com.russhwolf.settings.Settings
sealed class Screen {
data object Login : Screen()
data object Schedule : Screen()
data class EventDetail(val gameId: String, val account: String, val eventTitle: String) : Screen()
data class EventDetail(
val gameId: String,
val account: String,
val eventTitle: String,
val eventStart: String,
val eventEnd: String,
) : Screen()
}
@Composable
@@ -82,14 +88,16 @@ fun App(
Screen.Schedule -> ScheduleScreen(
scheduleViewModel = scheduleViewModel,
authViewModel = authViewModel,
onEventClick = { gameId, account, eventTitle ->
currentScreen = Screen.EventDetail(gameId, account, eventTitle)
onEventClick = { gameId, account, eventTitle, eventStart, eventEnd ->
currentScreen = Screen.EventDetail(gameId, account, eventTitle, eventStart, eventEnd)
},
)
is Screen.EventDetail -> EventDetailScreen(
gameId = screen.gameId,
account = screen.account,
eventTitle = screen.eventTitle,
eventStart = screen.eventStart,
eventEnd = screen.eventEnd,
viewModel = eventDetailViewModel,
onBack = { currentScreen = Screen.Schedule },
)
@@ -80,7 +80,7 @@ fun EventCard(
style = MaterialTheme.typography.bodySmall,
)
Text(
text = formatEventDateTime(event.start, event.end),
text = "Date: ${formatEventDateTime(event.start, event.end)}",
style = MaterialTheme.typography.bodySmall,
)
}
@@ -44,6 +44,8 @@ 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.util.formatEventDate
import ch.parano.myice.util.formatEventTimeRange
import ch.parano.myice.viewmodel.EventDetailViewModel
@OptIn(ExperimentalMaterial3Api::class)
@@ -52,6 +54,8 @@ fun EventDetailScreen(
gameId: String,
account: String,
eventTitle: String,
eventStart: String,
eventEnd: String,
viewModel: EventDetailViewModel,
onBack: () -> Unit,
modifier: Modifier = Modifier,
@@ -113,7 +117,8 @@ fun EventDetailScreen(
) {
Text("Type: ${detail.type}")
Text("Lieu: ${detail.place}")
Text("Heure: ${detail.timeStart} - ${detail.timeEnd}")
Text("Date: ${formatEventDate(eventStart)}")
Text("Heure: ${formatEventTimeRange(eventStart, eventEnd)}")
}
}
}
@@ -65,7 +65,7 @@ import ch.parano.myice.viewmodel.ScheduleViewModel
fun ScheduleScreen(
scheduleViewModel: ScheduleViewModel,
authViewModel: AuthViewModel,
onEventClick: (gameId: String, account: String, eventTitle: String) -> Unit,
onEventClick: (gameId: String, account: String, eventTitle: String, eventStart: String, eventEnd: String) -> Unit,
modifier: Modifier = Modifier,
) {
val scheduleState by scheduleViewModel.state.collectAsState()
@@ -250,6 +250,8 @@ fun ScheduleScreen(
event.idEvent,
scheduleState.selectedAccount!!,
event.title,
event.start,
event.end,
)
},
)
@@ -19,23 +19,43 @@ package ch.parano.myice.util
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 fun parseOrNull(iso: String): LocalDateTime? = runCatching {
LocalDateTime.parse(iso)
LocalDateTime.parse(iso.replace(' ', 'T'))
}.getOrNull()
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 =
"$day ${FRENCH_MONTHS[monthNumber - 1]} $year"
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 {
val startDt = parseOrNull(start)
val endDt = parseOrNull(end)
if (startDt == null || endDt == null) return "$start - $end"
return if (startDt.date == endDt.date) {
"${startDt.dateStr()} ${startDt.timeStr()} - ${endDt.timeStr()}"
"${startDt.dateStrLong()} de ${startDt.timeStr()} à ${endDt.timeStr()}"
} 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()}"
}
@@ -28,7 +28,16 @@ class EventDateTimeFormatterTest {
start = "2024-11-10T14:00:00",
end = "2024-11-10T16:15:00",
)
assertEquals("10.11.2024 14:00 - 16:15", formatted)
assertEquals("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("9 août 2026 de 10h à 12h", formatted)
}
@Test
@@ -37,7 +46,7 @@ class EventDateTimeFormatterTest {
start = "2024-11-10T20:00:00",
end = "2024-11-11T01:30:00",
)
assertEquals("10.11.2024 20:00 - 01:30 (11.11.2024)", formatted)
assertEquals("10 novembre 2024 de 20h à 1h30 (11 novembre 2024)", formatted)
}
@Test
@@ -6,7 +6,7 @@
## 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).