Compare commits
3 Commits
765fe51720
...
64ba5aea5d
| Author | SHA1 | Date | |
|---|---|---|---|
|
64ba5aea5d
|
|||
|
de107a4295
|
|||
|
8c2de8b475
|
@@ -6,5 +6,7 @@ local.properties
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.apk
|
*.apk
|
||||||
*.aab
|
*.aab
|
||||||
|
*.keystore
|
||||||
|
keystore.properties
|
||||||
*.xcworkspace/xcuserdata/
|
*.xcworkspace/xcuserdata/
|
||||||
iosApp/iosApp.xcodeproj/xcuserdata/
|
iosApp/iosApp.xcodeproj/xcuserdata/
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile (desktop)
|
||||||
|
./gradlew composeApp:compileKotlinDesktop
|
||||||
|
|
||||||
|
# Run desktop app
|
||||||
|
./gradlew composeApp:run
|
||||||
|
|
||||||
|
# Tests (desktop target — KMP has no commonTest task)
|
||||||
|
./gradlew composeApp:desktopTest
|
||||||
|
|
||||||
|
# Android debug APK
|
||||||
|
./gradlew composeApp:assembleDebug
|
||||||
|
|
||||||
|
# Android release APK (requires keystore.properties at project root)
|
||||||
|
./gradlew composeApp:assembleRelease
|
||||||
|
|
||||||
|
# macOS DMG
|
||||||
|
./gradlew composeApp:packageDistributionForCurrentOS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lint / Typecheck
|
||||||
|
|
||||||
|
There is no separate lint task. Compilation is the type check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew composeApp:compileKotlinDesktop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Kotlin 2.3.20, Compose Multiplatform 1.11.1, Material 3
|
||||||
|
- Ktor 3.5.1 (CIO on Android/Desktop, Darwin on iOS)
|
||||||
|
- kotlinx.serialization 1.11.0
|
||||||
|
- multiplatform-settings 1.3.0
|
||||||
|
- lifecycle-viewmodel-compose 2.10.0
|
||||||
|
- Gradle 9.5.1, AGP 8.13.2
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Layered: `models → network → cache → filter → viewmodel → ui`
|
||||||
|
|
||||||
|
- **commonMain**: all shared code (models, networking, caching, filtering, viewmodels, UI)
|
||||||
|
- **androidMain**: Android OAuth (Custom Tabs), MainActivity, CIO engine
|
||||||
|
- **iosMain**: iOS OAuth (ASWebAuthenticationSession), MainViewController, Darwin engine
|
||||||
|
- **desktopMain**: Desktop OAuth (local HTTP server), Main.kt, CIO engine
|
||||||
|
|
||||||
|
Package: `ch.parano.myice`
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Single `composeApp` module, shared UI approach (not per-platform UI)
|
||||||
|
- French UI strings (matching the Flutter app)
|
||||||
|
- TDD: write tests in `commonTest`, run via `desktopTest` target
|
||||||
|
- `expect/actual` for platform-specific code (HttpClient engine, OAuth)
|
||||||
|
- No comments in code unless explicitly requested
|
||||||
|
- Follow existing file structure: one class per file, organized by layer
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
API at `https://myice.parano.ch` (hardcoded in `network/ApiConfig.kt`).
|
||||||
|
Endpoints: `/login`, `/userinfo`, `/accounts`, `/schedule?account=`, `/game/{id}?account=`
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
- `keystore.properties` and `*.keystore` are gitignored — never commit
|
||||||
|
- OAuth tokens stored via multiplatform-settings (plaintext, same as Flutter app)
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- iOS `.xcodeproj` must be created manually in Xcode (Swift files exist in `iosApp/`)
|
||||||
|
- ViewModels use `remember{}` instead of `viewModel()` (state lost on Android rotation)
|
||||||
|
- No URL-decoding in token parser (JWT tokens don't need it)
|
||||||
|
- `AuthViewModel.init()` logs out on any network error, not just 401
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run directly
|
||||||
|
./gradlew composeApp:run
|
||||||
|
|
||||||
|
# Build distributable DMG
|
||||||
|
./gradlew composeApp:packageDistributionForCurrentOS
|
||||||
|
# Output: composeApp/build/compose/binaries/main/dmg/myice-1.0.0.dmg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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):
|
||||||
|
|
||||||
|
```properties
|
||||||
|
storeFile=myice.keystore
|
||||||
|
storePassword=<your-password>
|
||||||
|
keyAlias=<your-alias>
|
||||||
|
keyPassword=<your-password>
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a keystore:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./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
|
||||||
|
|
||||||
|
Private project.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.multiplatform)
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
@@ -9,6 +11,12 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
androidTarget {
|
androidTarget {
|
||||||
compilations.all {
|
compilations.all {
|
||||||
@@ -86,6 +94,22 @@ android {
|
|||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
keystoreProperties["storeFile"]?.let { storeFile = rootProject.file(it) }
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String?
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
isMinifyEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
Reference in New Issue
Block a user