Author SHA1 Message Date
herel efaa93d907 chore: add targets to makefile 2026-08-06 15:28:20 +02:00
herel 418a8978f2 ref: Rename package ch.parano.myice to ch.parano.myicek
Complete the MyIceK rename by updating the Kotlin package from ch.parano.myice to ch.parano.myicek across all source sets (androidMain, commonMain, desktopMain, iosMain, commonTest), the build.gradle.kts (namespace, applicationId, mainClass, desktop packageName) and the iOS Info.plist bundle URL name. The myice:// OAuth scheme is kept unchanged as it depends on the backend.

Also add pull-to-refresh to ScheduleScreen using Material 3 PullToRefreshBox, replacing the manual loading overlay.
2026-08-06 15:20:47 +02:00
herel a2d5a57aec feat(ui): Rename app from MyIce to MyIceK
Update the app display name in all user-visible locations:
Android manifest label, desktop window title, login screen,
and schedule screen top bar.

Refs #2
2026-08-06 13:49:35 +02:00
herel ea15c0423c feat(ui): Display staff in single card with aligned columns
Replace per-staff cards with a single card listing all staff. Each
line shows the name on the left and role in muted color on the right,
matching the players section style.
2026-08-06 11:39:50 +02:00
herel 3dc5890a7a feat(ui): Display players in single card with aligned columns
Replace per-player cards with a single card listing all players. Each
player shows on one line: number, name, position, and birth year in
muted color. Add Makefile with android and desktop targets.
2026-08-04 12:24:51 +02:00
herel fd8fcb9b4c fix(ui): Wire system back gesture to in-app navigation
Add PlatformBackHandler (expect/actual) to intercept the back gesture
on Android via BackHandler, and enable OnBackInvokedCallback in the
manifest so the swipe-back gesture triggers onBack instead of exiting
the app.

Fixes the back gesture exiting to home from the event detail screen.
2026-08-04 12:02:30 +02:00
herel 1e5c2fd864 chore: remove mention of myice_mobile 2026-08-04 11:15:05 +02:00
herel 9524bc93ca feat: show nice dates in events 2026-08-04 11:14:51 +02:00
herel 1795337d01 Merge pull request 'fix(ui): Show event dates in card view' (#1) from fix/event-card-date-display into main
Reviewed-on: #1
2026-08-04 07:59:29 +00:00
57 changed files with 347 additions and 167 deletions
-1
View File
@@ -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
+16
View File
@@ -0,0 +1,16 @@
.PHONY: android desktop android-usb android-emulator
ADB := $(shell command -v adb 2>/dev/null || echo "$(HOME)/Library/Android/sdk/platform-tools/adb")
APK := composeApp/build/outputs/apk/debug/composeApp-debug.apk
android:
./gradlew composeApp:assembleDebug
desktop:
./gradlew composeApp:run
android-usb: android
$(ADB) -d install -r $(APK)
android-emulator: android
$(ADB) -e install -r $(APK)
+1 -1
View File
@@ -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
+4 -4
View File
@@ -83,11 +83,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 +118,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"
} }
} }
@@ -4,9 +4,10 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="MyIce" android:label="MyIceK"
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"
@@ -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?) {
@@ -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?) {
@@ -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
@@ -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,26 @@
// 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.activity.compose.BackHandler
import androidx.compose.runtime.Composable
@Composable
actual fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit) {
BackHandler(enabled = enabled, onBack = onBack)
}
@@ -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 package ch.parano.myicek
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -24,25 +24,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
@@ -82,14 +88,16 @@ fun App(
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 },
) )
@@ -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) {
@@ -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
@@ -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 ->
@@ -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
@@ -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(" - ")
@@ -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;
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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
@@ -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
@@ -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)
@@ -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.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -33,8 +33,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.parano.myice.models.Event import ch.parano.myicek.models.Event
import ch.parano.myice.util.formatEventDateTime import ch.parano.myicek.util.formatEventDateTime
@Composable @Composable
fun EventCard( fun EventCard(
@@ -80,7 +80,7 @@ fun EventCard(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
Text( Text(
text = formatEventDateTime(event.start, event.end), text = "Date: ${formatEventDateTime(event.start, event.end)}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
} }
@@ -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.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@@ -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
@@ -15,15 +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.ui.screens package ch.parano.myicek.ui.screens
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
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.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -43,8 +45,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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.ui.components.LoadingIndicator import ch.parano.myicek.ui.PlatformBackHandler
import ch.parano.myice.viewmodel.EventDetailViewModel import ch.parano.myicek.ui.components.LoadingIndicator
import ch.parano.myicek.util.formatEventDate
import ch.parano.myicek.util.formatEventTimeRange
import ch.parano.myicek.viewmodel.EventDetailViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -52,10 +57,14 @@ fun EventDetailScreen(
gameId: String, gameId: String,
account: String, account: String,
eventTitle: String, eventTitle: String,
eventStart: String,
eventEnd: String,
viewModel: EventDetailViewModel, viewModel: EventDetailViewModel,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
PlatformBackHandler { onBack() }
LaunchedEffect(gameId, account) { LaunchedEffect(gameId, account) {
viewModel.loadEventDetail(gameId, account) viewModel.loadEventDetail(gameId, account)
} }
@@ -113,7 +122,8 @@ fun EventDetailScreen(
) { ) {
Text("Type: ${detail.type}") Text("Type: ${detail.type}")
Text("Lieu: ${detail.place}") Text("Lieu: ${detail.place}")
Text("Heure: ${detail.timeStart} - ${detail.timeEnd}") Text("Date: ${formatEventDate(eventStart)}")
Text("Heure: ${formatEventTimeRange(eventStart, eventEnd)}")
} }
} }
} }
@@ -142,18 +152,31 @@ fun EventDetailScreen(
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
) )
} }
items(detail.convocation.available) { player -> item {
val position = player.position ?: "N/A"
val number = player.number ?: "N/A"
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Column( Column(modifier = Modifier.padding(16.dp)) {
modifier = Modifier.padding(16.dp), detail.convocation.available.forEach { player ->
) { val position = player.position ?: ""
Text("[$position] #$number - ${player.fname} ${player.lname}") val number = player.number ?: ""
Text( val muted = MaterialTheme.colorScheme.onSurfaceVariant
text = "DOB: ${player.dob}", Row(
style = MaterialTheme.typography.bodySmall, verticalAlignment = Alignment.CenterVertically,
) horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth(),
) {
Text(
if (number.isNotEmpty()) "#$number" else "",
color = muted,
modifier = Modifier.width(40.dp),
)
Text("${player.fname} ${player.lname}")
Spacer(Modifier.weight(1f))
if (position.isNotEmpty()) {
Text(position, color = muted)
}
Text("(${player.dob.take(4)})", color = muted)
}
}
} }
} }
} }
@@ -166,12 +189,22 @@ fun EventDetailScreen(
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
) )
} }
items(detail.convocation.staff) { staff -> item {
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Text( Column(modifier = Modifier.padding(16.dp)) {
text = "${staff.role}: ${staff.fname} ${staff.lname}", detail.convocation.staff.forEach { staff ->
modifier = Modifier.padding(16.dp), val muted = MaterialTheme.colorScheme.onSurfaceVariant
) Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth(),
) {
Text("${staff.fname} ${staff.lname}")
Spacer(Modifier.weight(1f))
Text(staff.role, color = muted)
}
}
}
} }
} }
} }
@@ -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.screens package ch.parano.myicek.ui.screens
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -39,8 +39,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.viewmodel.AuthViewModel
import ch.parano.myice.viewmodel.AuthState import ch.parano.myicek.viewmodel.AuthState
@Composable @Composable
fun LoginScreen( fun LoginScreen(
@@ -66,7 +66,7 @@ fun LoginScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text( Text(
text = "MyIce", text = "MyIceK",
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
@@ -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.screens package ch.parano.myicek.ui.screens
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
@@ -31,7 +31,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -53,19 +53,19 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import ch.parano.myice.filter.TypeFilter import ch.parano.myicek.filter.TypeFilter
import ch.parano.myice.ui.components.EventCard import ch.parano.myicek.ui.components.EventCard
import ch.parano.myice.ui.components.FilterDropdown import ch.parano.myicek.ui.components.FilterDropdown
import ch.parano.myice.ui.components.LoadingIndicator import ch.parano.myicek.ui.components.LoadingIndicator
import ch.parano.myice.viewmodel.AuthViewModel import ch.parano.myicek.viewmodel.AuthViewModel
import ch.parano.myice.viewmodel.ScheduleViewModel import ch.parano.myicek.viewmodel.ScheduleViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ScheduleScreen( fun ScheduleScreen(
scheduleViewModel: ScheduleViewModel, scheduleViewModel: ScheduleViewModel,
authViewModel: AuthViewModel, 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, modifier: Modifier = Modifier,
) { ) {
val scheduleState by scheduleViewModel.state.collectAsState() val scheduleState by scheduleViewModel.state.collectAsState()
@@ -80,9 +80,9 @@ fun ScheduleScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("MyIce") }, title = { Text("MyIceK") },
actions = { actions = {
val email = (authState as? ch.parano.myice.viewmodel.AuthState.Authenticated)?.email val email = (authState as? ch.parano.myicek.viewmodel.AuthState.Authenticated)?.email
if (email != null) { if (email != null) {
Text( Text(
text = email, text = email,
@@ -201,7 +201,9 @@ fun ScheduleScreen(
} }
} }
} }
Box( PullToRefreshBox(
isRefreshing = scheduleState.isRefreshing,
onRefresh = { scheduleViewModel.refreshSchedule() },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
@@ -250,6 +252,8 @@ fun ScheduleScreen(
event.idEvent, event.idEvent,
scheduleState.selectedAccount!!, scheduleState.selectedAccount!!,
event.title, event.title,
event.start,
event.end,
) )
}, },
) )
@@ -257,23 +261,6 @@ fun ScheduleScreen(
} }
} }
} }
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)
}
}
}
}
} }
} }
} }
@@ -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.theme package ch.parano.myicek.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -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()}"
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@@ -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
@@ -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
@@ -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)
}
} }
@@ -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
@@ -15,14 +15,14 @@
// 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.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import ch.parano.myice.auth.DesktopOAuthClient import ch.parano.myicek.auth.DesktopOAuthClient
fun main() = application { fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "MyIce") { Window(onCloseRequest = ::exitApplication, title = "MyIceK") {
App(oauthClient = DesktopOAuthClient()) App(oauthClient = DesktopOAuthClient())
} }
} }
@@ -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
@@ -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) {
}
@@ -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())
@@ -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
@@ -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) {
}
@@ -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).
+1 -1
View File
@@ -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>