herel 057be15822 fix(desktop): Persist window size and position across restarts
The desktop window always reset to its default size after restart
because no WindowState was persisted. Add WindowStateStorage backed
by multiplatform-settings (java.util.prefs on desktop) to save and
restore width, height, and position. Use snapshotFlow inside the
Window composition to persist changes continuously, with an explicit
Preferences.flush() to ensure values reach disk before exit.

Also add 'desktop-install' Makefile target to rebuild and copy the
app to /Applications/myicek.app.
2026-08-11 17:11:58 +02:00
2026-07-09 16:19:38 +02:00

MyIce Kotlin Multiplatform

Ice hockey schedule and convocation viewer for Android, iOS, and Desktop (macOS/Windows/Linux). Kotlin Multiplatform with Compose Multiplatform.

Features

  • OAuth login via Infomaniak OIDC (platform-specific: Custom Tabs on Android, ASWebAuthenticationSession on iOS, local HTTP server on Desktop)
  • Schedule viewing — games and practices with colored event cards
  • Filtering — by account, age group, sub-group, and event type (all/games/practices)
  • Event details — convocations with player rosters and staff lists
  • Offline caching — schedule data persisted locally for offline access
  • Sub-group extraction — automatically parses team sub-groups from event names

Tech Stack

Component Technology
UI Compose Multiplatform 1.11.1 (Material 3)
Language Kotlin 2.3.20
Networking Ktor 3.5.1 (CIO on Android/Desktop, Darwin on iOS)
Serialization kotlinx.serialization 1.11.0
State androidx.lifecycle ViewModel + StateFlow
Storage multiplatform-settings 1.3.0
Build Gradle 9.5.1, AGP 8.13.2

Project Structure

composeApp/
├── src/
│   ├── commonMain/kotlin/ch/parano/myice/
│   │   ├── App.kt                      # Root composable + routing
│   │   ├── models/                     # Serializable data models
│   │   ├── network/                    # ApiService, AuthTokenHolder, ApiConfig
│   │   ├── auth/                       # OAuthClient interface, AuthSettings, TokenParser
│   │   ├── cache/                      # ScheduleCache (chunked storage)
│   │   ├── filter/                     # TypeFilter, EventFilter logic
│   │   ├── viewmodel/                  # AuthViewModel, ScheduleViewModel, EventDetailViewModel
│   │   └── ui/
│   │       ├── theme/                  # MyIceTheme (Material 3, indigo)
│   │       ├── components/             # EventCard, FilterDropdown, LoadingIndicator
│   │       └── screens/               # LoginScreen, ScheduleScreen, EventDetailScreen
│   ├── androidMain/                    # Android OAuth, MainActivity, CIO engine
│   ├── iosMain/                        # iOS OAuth, MainViewController, Darwin engine
│   ├── desktopMain/                    # Desktop OAuth, Main.kt, CIO engine
│   └── commonTest/                     # Unit tests (47 tests)
├── build.gradle.kts
└── src/androidMain/AndroidManifest.xml

Build & Run

Prerequisites

  • JDK 11+
  • Android SDK (for Android builds)
  • Xcode (for iOS builds, macOS only)
  • Kotlin Multiplatform Mobile plugin (for iOS)

Desktop (macOS)

# Run directly
./gradlew composeApp:run

# Build distributable DMG
./gradlew composeApp:packageDistributionForCurrentOS
# Output: composeApp/build/compose/binaries/main/dmg/myice-1.0.0.dmg

Android

# Debug APK
./gradlew composeApp:assembleDebug

# Release APK (requires keystore, see below)
./gradlew composeApp:assembleRelease

# Install and launch on connected device
~/Library/Android/sdk/platform-tools/adb install -r composeApp/build/outputs/apk/debug/composeApp-debug.apk
~/Library/Android/sdk/platform-tools/adb shell am start -n ch.parano.myice/.MainActivity

Release Signing

Create keystore.properties at the project root (gitignored):

storeFile=myice.keystore
storePassword=<your-password>
keyAlias=<your-alias>
keyPassword=<your-password>

Generate a keystore:

keytool -genkeypair -v \
  -keystore myice.keystore \
  -alias myice \
  -keyalg RSA -keysize 2048 \
  -validity 10000

iOS

The Swift source files and Info.plist are in iosApp/. The Xcode project (.xcodeproj) must be created manually:

  1. Open Xcode → New Project → iOS App
  2. Name it MyIceApp, bundle ID ch.parano.myice
  3. Add the Swift files from iosApp/
  4. Set the custom URL scheme myice in the URL Types
  5. Build and run on a simulator or device

Tests

./gradlew composeApp:desktopTest

Architecture

┌─────────────────────────────────┐
│           UI (Compose)          │
│  LoginScreen  ScheduleScreen    │
│           EventDetailScreen     │
└──────────────┬──────────────────┘
               │ StateFlow
┌──────────────┴──────────────────┐
│         ViewModels              │
│  AuthVM  ScheduleVM  EventDetVM │
└──────────────┬──────────────────┘
               │
┌──────────────┴──────────────────┐
│     AuthSettings + Cache        │
│  (multiplatform-settings)       │
└──────────────┬──────────────────┘
               │
┌──────────────┴──────────────────┐
│        ApiService (Ktor)        │
│  /userinfo  /accounts  /schedule│
│  /game/{id}                     │
└─────────────────────────────────┘

Backend

The app communicates with the MyIce FastAPI backend at https://myice.parano.ch.

Endpoint Description
GET /login OAuth login redirect (Infomaniak OIDC)
GET /userinfo Current user info (email)
GET /accounts Available accounts/teams
GET /schedule?account= Events for an account
GET /game/{id}?account= Event detail with convocation

License

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 v3.0 as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

S
Description
No description provided
Readme GPL-3.0
512 KiB
Languages
Kotlin 98%
Swift 1.2%
Makefile 0.8%