diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml
new file mode 100644
index 0000000..4b8ad3a
--- /dev/null
+++ b/.github/workflows/PR.yml
@@ -0,0 +1,84 @@
+name: PR job
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - development
+ pull_request:
+ paths-ignore:
+ - "**.md"
+ - "*.png"
+ - docs
+
+jobs:
+ pre-conditions:
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3.13.0
+ with:
+ distribution: corretto
+ java-version: 18
+
+ - name: detekt
+ run: ./gradlew detekt --stacktrace
+
+ - name: GitHub Action for SwiftLint (Only files changed in the PR)
+ uses: norio-nomura/action-swiftlint@3.2.1
+ env:
+ WORKING_DIRECTORY: ./iosApp
+
+ build-android:
+ needs: pre-conditions
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3.13.0
+ with:
+ distribution: corretto
+ java-version: 18
+
+ - name: Build
+ run: ./gradlew build --stacktrace
+
+ build-ios:
+ needs: pre-conditions
+ runs-on: macos-13
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3.13.0
+ with:
+ distribution: corretto
+ java-version: 18
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x ./gradlew
+
+ - name: Select Xcode version
+ run: |
+ XCODE_VERSION="15.0"
+ sudo xcode-select -s "/Applications/Xcode_${XCODE_VERSION}.app"
+
+ - name: Build
+ run: |
+ cd iosApp
+ rm -f iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+ xcodebuild -resolvePackageDependencies -project iosApp.xcodeproj
+ xcodebuild build-for-testing \
+ -scheme "iosApp" \
+ -project iosApp.xcodeproj \
+ -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' \
+ -allowProvisioningUpdates \
+ -configuration Debug \
+ DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }} # Reference the GitHub secret here
diff --git a/README.md b/README.md
index 8d4abc1..8a00d1b 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
EAPlayers is a Kotlin Multiplatform project that leverages **Compose Multiplatform** for both Android and iOS to provide a fully native mobile app experience. The project uses [EA's Drop API](https://drop-api.ea.com) to fetch player information.
+![GitHub Actions build status](https://github.com/kaszabimre/EAPlayers/actions/workflows/PR.yml/badge.svg)
+
![iOS](https://img.shields.io/badge/iOS-000000?style=for-the-badge&logo=ios&logoColor=white)
![Android](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white)
![Kotlin](https://img.shields.io/badge/Kotlin-0095D5?&style=for-the-badge&logo=kotlin&logoColor=white)
diff --git a/build.gradle.kts b/build.gradle.kts
index 92ac0aa..828a720 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -55,3 +55,7 @@ allprojects {
jvmTarget = libs.versions.javaTargetCompatibility.get()
}
}
+
+dependencies {
+ detektPlugins(libs.detekt.formatting)
+}
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 25e2985..79a2624 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -29,7 +29,6 @@ kotlin {
baseName = "ComposeApp"
}
}
-
sourceSets {
androidMain.dependencies {
implementation(compose.preview)
@@ -48,14 +47,18 @@ kotlin {
implementation(libs.ktor.client.core)
implementation(libs.koin.core)
implementation(libs.koin.compose.multiplatform)
- implementation("co.touchlab:stately-common:2.0.5")
+ implementation(libs.stately.common)
implementation(libs.coil.compose.core)
implementation(libs.coil.compose)
implementation(libs.coil.mp)
implementation(libs.coil.network.ktor)
+ // Modules
implementation(projects.shared)
+ implementation(projects.modules.core)
+ implementation(projects.modules.domain)
+ implementation(projects.modules.theme)
}
}
}
@@ -96,6 +99,7 @@ android {
buildConfig = true
}
dependencies {
+ implementation(libs.androidx.compose.material)
implementation(libs.kotlinx.coroutines.android)
debugImplementation(compose.uiTooling)
implementation(libs.compose.ui)
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
new file mode 100644
index 0000000..7a3b329
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -0,0 +1,14 @@
+
+ Abilities
+ Rating
+ Position
+ Height
+ Team mates
+ Rank: %1$s
+ Nationality
+ Team
+ Rank
+ Rating: %1$s
+ Position: %1$s
+ %1$s %2$s's image
+
diff --git a/composeApp/src/commonMain/kotlin/PlayerDetailView.kt b/composeApp/src/commonMain/kotlin/PlayerDetailView.kt
deleted file mode 100644
index 1a1529a..0000000
--- a/composeApp/src/commonMain/kotlin/PlayerDetailView.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-import androidx.compose.foundation.background
-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.Spacer
-import androidx.compose.foundation.layout.aspectRatio
-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.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.CheckCircle
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.filled.Star
-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.draw.clip
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil3.compose.AsyncImage
-import io.imrekaszab.eaplayers.domain.model.Player
-import widget.AbilityItemView
-import widget.PlayerStatItem
-
-@Composable
-fun PlayerDetailView(
- modifier: Modifier = Modifier,
- player: Player,
- onTeamMateSelected: (Player) -> Unit
-) {
- Column(
- modifier = modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(horizontal = 16.dp)
- .background(MaterialTheme.colorScheme.background)
- ) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 16.dp)
- .aspectRatio(1f)
- .clip(RoundedCornerShape(16.dp))
- .background(MaterialTheme.colorScheme.surface)
- ) {
- AsyncImage(
- model = player.shieldUrl,
- contentDescription = "${player.firstName} ${player.lastName}",
- modifier = Modifier.fillMaxSize(),
- )
- }
-
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = "${player.firstName} ${player.lastName}",
- style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
- color = MaterialTheme.colorScheme.onBackground
- )
- Text(
- text = "Rank: ${player.rank}",
- style = MaterialTheme.typography.bodyLarge.copy(fontSize = 18.sp),
- color = MaterialTheme.colorScheme.secondary
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly,
- ) {
- PlayerStatItem(
- imageUrl = player.nationality.imageUrl,
- stat = player.nationality.label,
- label = "Nationality"
- )
- PlayerStatItem(
- imageUrl = player.team.imageUrl,
- stat = player.team.label,
- label = "Team"
- )
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- PlayerStatItem(
- icon = Icons.Default.Star,
- stat = player.overallRating.toString(),
- label = "Rating"
- )
- PlayerStatItem(
- icon = Icons.Default.Menu,
- stat = player.position.shortLabel,
- label = "Position"
- )
- PlayerStatItem(
- icon = Icons.Default.CheckCircle,
- stat = "${player.height} cm",
- label = "Height"
- )
- }
-
- Spacer(modifier = Modifier.height(24.dp))
-
- if (player.playerAbilities.isNotEmpty()) {
- Text(
- text = "Abilities",
- style = MaterialTheme.typography.headlineMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
- Spacer(modifier = Modifier.height(8.dp))
-
- player.playerAbilities.forEach { ability ->
- AbilityItemView(ability)
- }
- }
-
- Spacer(modifier = Modifier.height(24.dp))
-
- Text(
- text = "Team mates",
- style = MaterialTheme.typography.headlineMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
- Spacer(modifier = Modifier.height(8.dp))
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(400.dp)
- ) {
- LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 100.dp)) {
- items(
- player.teamMates.filter { it.id != player.id }
- .sortedByDescending { it.overallRating }
- ) { mate ->
- Column(
- modifier = Modifier
- .padding(8.dp)
- .width(width = 80.dp)
- .clickable {
- onTeamMateSelected(mate)
- },
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- AsyncImage(
- modifier = Modifier
- .size(50.dp)
- .clip(CircleShape),
- model = mate.avatarUrl,
- contentDescription = mate.lastName
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = mate.commonName
- ?: (mate.firstName + ". " + mate.lastName.first()),
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.bodySmall.copy(
- fontWeight = FontWeight.Bold
- ),
- color = MaterialTheme.colorScheme.onSurface
- )
- Text(
- text = mate.position.shortLabel,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurface
- )
- }
- }
- }
- }
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/PlayerItemView.kt b/composeApp/src/commonMain/kotlin/PlayerItemView.kt
deleted file mode 100644
index 1327f99..0000000
--- a/composeApp/src/commonMain/kotlin/PlayerItemView.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-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.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import coil3.compose.AsyncImage
-import io.imrekaszab.eaplayers.domain.model.Player
-
-@Composable
-fun PlayerItemView(player: Player, onPlayerClick: (Player) -> Unit) {
- Card(
- shape = RoundedCornerShape(12.dp),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant
- ),
- modifier = Modifier
- .padding(12.dp)
- .fillMaxWidth()
- .clickable {
- onPlayerClick(player)
- }
- ) {
- Row(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.surface)
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- AsyncImage(
- model = player.avatarUrl,
- contentDescription = "${player.firstName} ${player.lastName}",
- modifier = Modifier
- .size(100.dp)
- .padding(end = 16.dp),
- )
-
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = player.commonName ?: "${player.firstName} ${player.lastName}",
- style = MaterialTheme.typography.titleMedium.copy(
- fontWeight = FontWeight.Bold
- ),
- color = MaterialTheme.colorScheme.onSurface
- )
- Text(
- text = "Rating: ${player.overallRating}",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Text(
- text = "Position: ${player.position.label}",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.secondary
- )
- }
-
- Surface(
- shape = RoundedCornerShape(16.dp),
- color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(50.dp)
- ) {
- Column(
- modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = "Rank",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onPrimary,
- )
- Text(
- text = player.rank.toString(),
- color = MaterialTheme.colorScheme.onPrimary,
- style = MaterialTheme.typography.titleMedium
- )
- }
- }
- }
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/PlayerListScreen.kt b/composeApp/src/commonMain/kotlin/PlayerListScreen.kt
deleted file mode 100644
index ef3278e..0000000
--- a/composeApp/src/commonMain/kotlin/PlayerListScreen.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-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.CircularProgressIndicator
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-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.unit.dp
-import androidx.navigation.NavHostController
-import io.imrekaszab.eaplayers.core.util.collectAsStateInLifecycle
-import io.imrekaszab.eaplayers.core.util.invoke
-import io.imrekaszab.eaplayers.viewmodel.PlayerListViewModel
-import navigation.EAPlayersScreens
-
-@Composable
-fun PlayerListScreen(
- viewModel: PlayerListViewModel,
- navHostController: NavHostController
-) {
- val uiState by viewModel.uiState.collectAsStateInLifecycle()
-
- var searchQuery by remember { mutableStateOf("") }
-
- LaunchedEffect(searchQuery) {
- viewModel.refreshPlayers(searchQuery)
- }
- LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- item {
- TextField(
- modifier = Modifier.fillMaxWidth().padding(8.dp),
- value = searchQuery,
- onValueChange = { searchQuery = it }
- )
- }
- if (uiState.loading) {
- item {
- Box(modifier = Modifier.fillMaxSize()) {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
- }
- }
- } else {
- items(uiState.players) { player ->
- PlayerItemView(
- player = player,
- onPlayerClick = {
- viewModel.selectPlayer(player)
- navHostController.navigate(EAPlayersScreens.DetailsScreen.createRoute(player.id))
- }
- )
- }
- }
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/navigation/Navigation.kt b/composeApp/src/commonMain/kotlin/navigation/Navigation.kt
index 907fee1..6904b61 100644
--- a/composeApp/src/commonMain/kotlin/navigation/Navigation.kt
+++ b/composeApp/src/commonMain/kotlin/navigation/Navigation.kt
@@ -1,87 +1,51 @@
package navigation
-import PlayerDetailScreen
-import PlayerListScreen
import androidx.compose.animation.AnimatedContentTransitionScope
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.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.SheetState
-import androidx.compose.material3.Switch
-import androidx.compose.material3.SwitchDefaults
-import androidx.compose.material3.Text
-import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import io.imrekaszab.eaplayers.core.util.collectAsStateInLifecycle
-import io.imrekaszab.eaplayers.core.util.invoke
-import io.imrekaszab.eaplayers.core.util.launchOnDefault
-import io.imrekaszab.eaplayers.viewmodel.AppViewModel
+import io.imrekaszab.eaplayers.theme.AppTheme
import io.imrekaszab.eaplayers.viewmodel.PlayerListViewModel
-import kotlinx.coroutines.CoroutineScope
import org.koin.compose.koinInject
-import theme.AppTheme
+import screen.PlayerDetailScreen
+import screen.PlayerListScreen
+
+private const val ANIMATION_DURATION = 500
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Navigation() {
val navController = rememberNavController()
- val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- val viewModel = koinInject()
val listviewModel: PlayerListViewModel = koinInject()
- val coroutineScope = rememberCoroutineScope()
- val uiState by viewModel.uiState.collectAsStateInLifecycle()
- AppTheme(darkTheme = uiState.darkMode, content = {
- ContainerScreen(content = {
+ AppTheme(
+ content = {
NavHost(
navController = navController,
startDestination = EAPlayersScreens.ListScreen.route,
enterTransition = {
slideIntoContainer(
AnimatedContentTransitionScope.SlideDirection.Left,
- animationSpec = tween(500)
+ animationSpec = tween(ANIMATION_DURATION)
)
},
exitTransition = {
slideOutOfContainer(
AnimatedContentTransitionScope.SlideDirection.Left,
- animationSpec = tween(500)
+ animationSpec = tween(ANIMATION_DURATION)
)
},
popEnterTransition = {
slideIntoContainer(
AnimatedContentTransitionScope.SlideDirection.Right,
- animationSpec = tween(500)
+ animationSpec = tween(ANIMATION_DURATION)
)
},
popExitTransition = {
slideOutOfContainer(
AnimatedContentTransitionScope.SlideDirection.Right,
- animationSpec = tween(500)
+ animationSpec = tween(ANIMATION_DURATION)
)
}
) {
@@ -95,98 +59,6 @@ fun Navigation() {
PlayerDetailScreen(navController)
}
}
- if (bottomSheetState.isVisible) {
- Modal(
- bottomSheetState = bottomSheetState,
- appViewModel = viewModel,
- isDarkMode = uiState.darkMode,
- coroutineScope = coroutineScope
- )
- }
- },
- onIconClick = {
- coroutineScope.launchOnDefault {
- if (!bottomSheetState.isVisible) {
- bottomSheetState.show()
- } else {
- bottomSheetState.hide()
- }
- }
- })
- }
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun Modal(
- bottomSheetState: SheetState,
- appViewModel: AppViewModel,
- isDarkMode: Boolean,
- coroutineScope: CoroutineScope
-) {
- ModalBottomSheet(
- sheetState = bottomSheetState,
- containerColor = MaterialTheme.colorScheme.background,
- onDismissRequest = {
- coroutineScope.launchOnDefault {
- bottomSheetState.hide()
- }
- }) {
- Column(
- modifier = Modifier.background(MaterialTheme.colorScheme.background)
- .padding(horizontal = 16.dp)
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 16.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- text = if (isDarkMode) "Dark Mode" else "Light Mode",
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.onBackground
- )
- Switch(
- checked = isDarkMode,
- onCheckedChange = { appViewModel.switchDarkMode() },
- colors = SwitchDefaults.colors(
- checkedThumbColor = MaterialTheme.colorScheme.primary,
- uncheckedThumbColor = MaterialTheme.colorScheme.onSurface,
- checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
- uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant
- )
- )
- }
- }
- }
-}
-
-@Composable
-fun ContainerScreen(
- content: @Composable () -> Unit,
- onIconClick: () -> Unit = {},
-) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background)
- ) {
- content()
- IconButton(
- modifier = Modifier
- .align(Alignment.BottomCenter).size(40.dp)
- .clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
- .background(MaterialTheme.colorScheme.primary),
- onClick = onIconClick
- ) {
- Icon(
- imageVector = Icons.Default.Menu,
- tint = MaterialTheme.colorScheme.secondary,
- contentDescription = null,
- )
}
- }
+ )
}
diff --git a/composeApp/src/commonMain/kotlin/PlayerDetailScreen.kt b/composeApp/src/commonMain/kotlin/screen/PlayerDetailScreen.kt
similarity index 86%
rename from composeApp/src/commonMain/kotlin/PlayerDetailScreen.kt
rename to composeApp/src/commonMain/kotlin/screen/PlayerDetailScreen.kt
index e437340..9a0e273 100644
--- a/composeApp/src/commonMain/kotlin/PlayerDetailScreen.kt
+++ b/composeApp/src/commonMain/kotlin/screen/PlayerDetailScreen.kt
@@ -1,10 +1,11 @@
+package screen
+
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
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.runtime.Composable
@@ -12,12 +13,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import io.imrekaszab.eaplayers.core.util.invoke
+import io.imrekaszab.eaplayers.theme.AppTheme
import io.imrekaszab.eaplayers.viewmodel.PlayerDetailViewModel
import navigation.EAPlayersScreens
import org.koin.compose.koinInject
+import widget.PlayerDetailView
@Composable
fun PlayerDetailScreen(
@@ -37,16 +39,18 @@ fun PlayerDetailScreen(
Scaffold(
topBar = {
IconButton(
- modifier = Modifier.padding(4.dp),
+ modifier = Modifier.padding(AppTheme.dimens.margin.extraTiny),
onClick = { navHostController.popBackStack() }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
- tint = MaterialTheme.colorScheme.onBackground
+ tint = AppTheme.colorScheme.onBackground
)
}
- }
+ },
+ containerColor = AppTheme.colorScheme.background,
+ contentColor = AppTheme.colorScheme.onPrimary
) { innerPadding ->
when {
uiState.loading -> CircularProgressIndicator()
diff --git a/composeApp/src/commonMain/kotlin/screen/PlayerListScreen.kt b/composeApp/src/commonMain/kotlin/screen/PlayerListScreen.kt
new file mode 100644
index 0000000..b81ed71
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/screen/PlayerListScreen.kt
@@ -0,0 +1,128 @@
+package screen
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.navigation.NavHostController
+import io.imrekaszab.eaplayers.core.util.collectAsStateInLifecycle
+import io.imrekaszab.eaplayers.core.util.invoke
+import io.imrekaszab.eaplayers.theme.AppTheme
+import io.imrekaszab.eaplayers.viewmodel.PlayerListViewModel
+import navigation.EAPlayersScreens
+import widget.PlayerItemView
+
+@Composable
+fun PlayerListScreen(
+ viewModel: PlayerListViewModel,
+ navHostController: NavHostController
+) {
+ val uiState by viewModel.uiState.collectAsStateInLifecycle()
+
+ var searchQuery by remember { mutableStateOf(viewModel.uiState.value.textFieldValue) }
+
+ LaunchedEffect(searchQuery) {
+ viewModel.refreshPlayers(searchQuery)
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(AppTheme.colorScheme.background)
+ ) {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(AppTheme.dimens.margin.tiny),
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ TextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(AppTheme.dimens.margin.tiny),
+ value = searchQuery,
+ textStyle = AppTheme.typography.body.medium,
+ onValueChange = { searchQuery = it },
+ colors = textFieldColors()
+ )
+ }
+
+ if (uiState.loading) {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(AppTheme.colorScheme.background)
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center),
+ color = AppTheme.colorScheme.onPrimary
+ )
+ }
+ }
+ } else {
+ items(uiState.players) { player ->
+ PlayerItemView(
+ player = player,
+ onPlayerClick = {
+ viewModel.selectPlayer(player)
+ navHostController.navigate(
+ EAPlayersScreens.DetailsScreen.createRoute(
+ player.id
+ )
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun textFieldColors() =
+ TextFieldDefaults.colors().copy(
+ // Text Colors
+ focusedTextColor = AppTheme.colorScheme.primaryContainer,
+ unfocusedTextColor = AppTheme.colorScheme.primaryContainer,
+ disabledTextColor = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f),
+ errorTextColor = AppTheme.colorScheme.error,
+
+ // Container Colors
+ focusedContainerColor = AppTheme.colorScheme.surface,
+ unfocusedContainerColor = AppTheme.colorScheme.surfaceVariant,
+ disabledContainerColor = AppTheme.colorScheme.surface.copy(alpha = 0.12f),
+ errorContainerColor = AppTheme.colorScheme.errorContainer,
+
+ // Cursor and Text Selection Colors
+ cursorColor = AppTheme.colorScheme.primary,
+ errorCursorColor = AppTheme.colorScheme.error,
+ textSelectionColors = LocalTextSelectionColors.current,
+
+ // Indicator (Underline) Colors
+ focusedIndicatorColor = AppTheme.colorScheme.primary,
+ unfocusedIndicatorColor = AppTheme.colorScheme.outline,
+ disabledIndicatorColor = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f),
+ errorIndicatorColor = AppTheme.colorScheme.error,
+
+ // Label Colors (Fix for Light Mode)
+ focusedLabelColor = AppTheme.colorScheme.onSurface,
+ unfocusedLabelColor = AppTheme.colorScheme.onSurfaceVariant,
+ disabledLabelColor = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f),
+ errorLabelColor = AppTheme.colorScheme.error
+ )
diff --git a/composeApp/src/commonMain/kotlin/theme/AppTheme.kt b/composeApp/src/commonMain/kotlin/theme/AppTheme.kt
deleted file mode 100644
index 4a47202..0000000
--- a/composeApp/src/commonMain/kotlin/theme/AppTheme.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package theme
-
-import AppTypography
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-
-@Composable
-fun AppTheme(
- darkTheme: Boolean = false,
- content: @Composable () -> Unit
-) {
- MaterialTheme(
- colorScheme = if (darkTheme) DarkColors else LightColors,
- typography = AppTypography(),
- shapes = AppShapes,
- content = content
- )
-}
diff --git a/composeApp/src/commonMain/kotlin/theme/Colors.kt b/composeApp/src/commonMain/kotlin/theme/Colors.kt
deleted file mode 100644
index 9b9f974..0000000
--- a/composeApp/src/commonMain/kotlin/theme/Colors.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package theme
-
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.ui.graphics.Color
-
-internal val LightColors = lightColorScheme(
- primary = Color(0xFF0D47A1), // Dark Blue
- onPrimary = Color.White,
- primaryContainer = Color(0xFF5472D3), // Lighter Blue
- onPrimaryContainer = Color.Black,
- secondary = Color(0xFFFFC107), // Yellow
- onSecondary = Color.Black,
- secondaryContainer = Color(0xFFFFE082), // Light Yellow
- onSecondaryContainer = Color.Black,
- background = Color(0xFFF6F6F6),
- onBackground = Color.Black,
- surface = Color.White,
- onSurface = Color.Black,
- error = Color(0xFFB00020),
- onError = Color.White,
-)
-
-internal val DarkColors = darkColorScheme(
- primary = Color(0xFF0D47A1), // Dark Blue
- onPrimary = Color.White,
- primaryContainer = Color(0xFF001970), // Even Darker Blue
- onPrimaryContainer = Color.White,
- secondary = Color(0xFFFFC107), // Yellow
- onSecondary = Color.Black,
- secondaryContainer = Color(0xFFFFB300), // Darker Yellow
- onSecondaryContainer = Color.Black,
- background = Color(0xFF121212),
- onBackground = Color.White,
- surface = Color(0xFF1D1D1D),
- onSurface = Color.White,
- error = Color(0xFFCF6679),
- onError = Color.Black,
-)
diff --git a/composeApp/src/commonMain/kotlin/theme/FontFamily.kt b/composeApp/src/commonMain/kotlin/theme/FontFamily.kt
deleted file mode 100644
index bc5bf7a..0000000
--- a/composeApp/src/commonMain/kotlin/theme/FontFamily.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package theme
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import eaplayers.composeapp.generated.resources.Poppins_Bold
-import eaplayers.composeapp.generated.resources.Poppins_Medium
-import eaplayers.composeapp.generated.resources.Poppins_Regular
-import eaplayers.composeapp.generated.resources.Poppins_SemiBold
-import eaplayers.composeapp.generated.resources.Res
-import org.jetbrains.compose.resources.Font
-
-@Composable
-fun getPoppins() = FontFamily(
- Font(Res.font.Poppins_Regular, FontWeight.Normal),
- Font(Res.font.Poppins_Medium, FontWeight.Medium),
- Font(Res.font.Poppins_SemiBold, FontWeight.SemiBold),
- Font(Res.font.Poppins_Bold, FontWeight.Bold)
-)
diff --git a/composeApp/src/commonMain/kotlin/theme/Shapes.kt b/composeApp/src/commonMain/kotlin/theme/Shapes.kt
deleted file mode 100644
index aaf8743..0000000
--- a/composeApp/src/commonMain/kotlin/theme/Shapes.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package theme
-
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Shapes
-import androidx.compose.ui.unit.dp
-
-val AppShapes = Shapes(
- small = RoundedCornerShape(4.dp),
- medium = RoundedCornerShape(8.dp),
- large = RoundedCornerShape(12.dp)
-)
diff --git a/composeApp/src/commonMain/kotlin/theme/Typography.kt b/composeApp/src/commonMain/kotlin/theme/Typography.kt
deleted file mode 100644
index f12c53a..0000000
--- a/composeApp/src/commonMain/kotlin/theme/Typography.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-import androidx.compose.material3.Typography
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-import theme.getPoppins
-
-@Composable
-fun AppTypography(): Typography {
- val poppins = getPoppins()
-
- return Typography(
- displayLarge = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Bold,
- fontSize = 57.sp,
- lineHeight = 64.sp,
- letterSpacing = (-0.25).sp
- ),
- displayMedium = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Medium,
- fontSize = 45.sp,
- lineHeight = 52.sp
- ),
- displaySmall = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Medium,
- fontSize = 36.sp,
- lineHeight = 44.sp
- ),
- headlineLarge = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.SemiBold,
- fontSize = 32.sp,
- lineHeight = 40.sp
- ),
- headlineMedium = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.SemiBold,
- fontSize = 28.sp,
- lineHeight = 36.sp
- ),
- headlineSmall = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.SemiBold,
- fontSize = 24.sp,
- lineHeight = 32.sp
- ),
- titleLarge = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Bold,
- fontSize = 22.sp,
- lineHeight = 28.sp
- ),
- titleMedium = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Medium,
- fontSize = 16.sp,
- lineHeight = 24.sp
- ),
- titleSmall = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.SemiBold,
- fontSize = 14.sp,
- lineHeight = 20.sp
- ),
- bodyLarge = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp
- ),
- bodyMedium = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Medium,
- fontSize = 14.sp,
- lineHeight = 20.sp
- ),
- bodySmall = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Normal,
- fontSize = 12.sp,
- lineHeight = 16.sp
- ),
- labelLarge = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Medium,
- fontSize = 14.sp,
- lineHeight = 20.sp
- ),
- labelMedium = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Medium,
- fontSize = 12.sp,
- lineHeight = 16.sp
- ),
- labelSmall = TextStyle(
- fontFamily = poppins,
- fontWeight = FontWeight.Normal,
- fontSize = 11.sp,
- lineHeight = 16.sp
- )
- )
-}
diff --git a/composeApp/src/commonMain/kotlin/widget/AbilityItemView.kt b/composeApp/src/commonMain/kotlin/widget/AbilityItemView.kt
index e8dc407..d4af1c3 100644
--- a/composeApp/src/commonMain/kotlin/widget/AbilityItemView.kt
+++ b/composeApp/src/commonMain/kotlin/widget/AbilityItemView.kt
@@ -6,34 +6,33 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-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.text.font.FontWeight
-import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import io.imrekaszab.eaplayers.domain.model.PlayerAbility
+import io.imrekaszab.eaplayers.theme.AppTheme
@Composable
fun AbilityItemView(ability: PlayerAbility) {
Row(
modifier = Modifier
.fillMaxWidth()
- .padding(vertical = 8.dp),
+ .padding(vertical = AppTheme.dimens.margin.tiny),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = ability.imageUrl,
contentDescription = ability.label,
- modifier = Modifier.size(40.dp)
+ modifier = Modifier.size(AppTheme.dimens.imageSize.abilityItemImageSize)
)
- Spacer(modifier = Modifier.width(16.dp))
+ Spacer(modifier = Modifier.width(AppTheme.dimens.margin.default))
Text(
text = ability.label,
- style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold),
- color = MaterialTheme.colorScheme.onBackground
+ style = AppTheme.typography.body.large.copy(fontWeight = FontWeight.Bold),
+ color = AppTheme.colors.yellow.light2
)
}
}
diff --git a/composeApp/src/commonMain/kotlin/widget/PlayerDetailView.kt b/composeApp/src/commonMain/kotlin/widget/PlayerDetailView.kt
new file mode 100644
index 0000000..0ab1473
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/widget/PlayerDetailView.kt
@@ -0,0 +1,118 @@
+package widget
+
+import androidx.compose.foundation.background
+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.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.style.TextAlign
+import eaplayers.composeapp.generated.resources.Res
+import eaplayers.composeapp.generated.resources.abilities
+import eaplayers.composeapp.generated.resources.nationality
+import eaplayers.composeapp.generated.resources.player_rank
+import eaplayers.composeapp.generated.resources.team
+import io.imrekaszab.eaplayers.domain.model.Player
+import io.imrekaszab.eaplayers.theme.AppTheme
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PlayerDetailView(
+ modifier: Modifier = Modifier,
+ player: Player,
+ onTeamMateSelected: (Player) -> Unit
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = AppTheme.dimens.margin.default)
+ .background(AppTheme.colorScheme.background)
+ ) {
+ PlayerImage(player = player)
+
+ PlayerInfoSection(player = player)
+
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.default))
+
+ PlayerStatsRow(player = player)
+
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.big))
+
+ AbilitiesSection(player = player)
+
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.big))
+
+ TeamMatesSection(
+ player = player,
+ onTeamMateSelected = onTeamMateSelected
+ )
+ }
+}
+
+@Composable
+fun PlayerInfoSection(player: Player) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "${player.firstName} ${player.lastName}",
+ style = AppTheme.typography.heading.large,
+ color = AppTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ text = stringResource(Res.string.player_rank, player.rank),
+ style = AppTheme.typography.heading.large,
+ color = AppTheme.colorScheme.secondary
+ )
+
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.default))
+
+ PlayerStatRow(
+ statItems = listOf(
+ PlayerStat(
+ player.nationality.imageUrl,
+ player.nationality.label,
+ stringResource(Res.string.nationality)
+ ),
+ PlayerStat(player.team.imageUrl, player.team.label, stringResource(Res.string.team))
+ )
+ )
+ }
+}
+
+@Composable
+fun AbilitiesSection(player: Player) {
+ Column(
+ modifier = Modifier
+ .clip(AppTheme.shapes.default.roundedDefault)
+ .background(
+ color = AppTheme.colors.blue.default,
+ shape = AppTheme.shapes.default.roundedDefault
+ )
+ .padding(AppTheme.dimens.margin.default)
+ ) {
+ if (player.playerAbilities.isNotEmpty()) {
+ Text(
+ text = stringResource(Res.string.abilities),
+ style = AppTheme.typography.heading.medium,
+ color = AppTheme.colors.yellow.default
+ )
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.tiny))
+
+ player.playerAbilities.forEach { ability ->
+ AbilityItemView(ability)
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/widget/PlayerImage.kt b/composeApp/src/commonMain/kotlin/widget/PlayerImage.kt
new file mode 100644
index 0000000..0c256c3
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/widget/PlayerImage.kt
@@ -0,0 +1,39 @@
+package widget
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import coil3.compose.AsyncImage
+import eaplayers.composeapp.generated.resources.Res
+import eaplayers.composeapp.generated.resources.player_image_desc
+import io.imrekaszab.eaplayers.domain.model.Player
+import io.imrekaszab.eaplayers.theme.AppTheme
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PlayerImage(player: Player) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = AppTheme.dimens.margin.default)
+ .aspectRatio(1f)
+ .clip(AppTheme.shapes.default.roundedDefault)
+ .background(AppTheme.colorScheme.surface)
+ ) {
+ AsyncImage(
+ model = player.shieldUrl,
+ contentDescription = stringResource(
+ Res.string.player_image_desc,
+ player.firstName,
+ player.lastName
+ ),
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/widget/PlayerItemView.kt b/composeApp/src/commonMain/kotlin/widget/PlayerItemView.kt
new file mode 100644
index 0000000..0cf61c9
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/widget/PlayerItemView.kt
@@ -0,0 +1,117 @@
+package widget
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+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.layout.size
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import coil3.compose.AsyncImage
+import eaplayers.composeapp.generated.resources.Res
+import eaplayers.composeapp.generated.resources.player_image_desc
+import eaplayers.composeapp.generated.resources.position_with_param
+import eaplayers.composeapp.generated.resources.rank
+import eaplayers.composeapp.generated.resources.rating_with_param
+import io.imrekaszab.eaplayers.domain.model.Player
+import io.imrekaszab.eaplayers.theme.AppTheme
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PlayerItemView(player: Player, onPlayerClick: (Player) -> Unit) {
+ Card(
+ shape = AppTheme.shapes.default.roundedSmall,
+ colors = CardDefaults.cardColors(
+ containerColor = AppTheme.colorScheme.surfaceVariant
+ ),
+ modifier = Modifier
+ .padding(AppTheme.dimens.margin.small)
+ .fillMaxWidth()
+ .clickable { onPlayerClick(player) }
+ ) {
+ Row(
+ modifier = Modifier
+ .background(AppTheme.colorScheme.surface)
+ .padding(AppTheme.dimens.margin.default),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ PlayerDetailImage(player = player)
+ PlayerInfo(modifier = Modifier.weight(1f), player = player)
+ PlayerRankBadge(player = player)
+ }
+ }
+}
+
+@Composable
+fun PlayerDetailImage(player: Player) {
+ AsyncImage(
+ model = player.avatarUrl,
+ contentDescription = stringResource(
+ Res.string.player_image_desc,
+ player.firstName,
+ player.lastName
+ ),
+ modifier = Modifier
+ .size(AppTheme.dimens.playerDetailView.playerItemImageSize)
+ .padding(end = AppTheme.dimens.margin.default)
+ )
+}
+
+@Composable
+fun PlayerInfo(modifier: Modifier, player: Player) {
+ Column(modifier = modifier) {
+ Text(
+ text = player.commonName ?: "${player.firstName} ${player.lastName}",
+ style = AppTheme.typography.heading.medium.copy(
+ fontWeight = FontWeight.Bold
+ ),
+ color = AppTheme.colorScheme.onSurface
+ )
+ Text(
+ text = stringResource(Res.string.rating_with_param, player.overallRating),
+ style = AppTheme.typography.body.medium,
+ color = AppTheme.colorScheme.onSurfaceVariant
+ )
+ Text(
+ text = stringResource(Res.string.position_with_param, player.position.label),
+ style = AppTheme.typography.body.small,
+ color = AppTheme.colorScheme.secondary
+ )
+ }
+}
+
+@Composable
+fun PlayerRankBadge(player: Player) {
+ Surface(
+ shape = AppTheme.shapes.default.roundedDefault,
+ color = AppTheme.colorScheme.primary,
+ modifier = Modifier.size(AppTheme.dimens.margin.extraLarge)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(Res.string.rank),
+ style = AppTheme.typography.body.extraSmall,
+ color = AppTheme.colors.blackAndWhite.white,
+ )
+ Text(
+ text = player.rank.toString(),
+ color = AppTheme.colors.blackAndWhite.white,
+ style = AppTheme.typography.body.large
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/widget/PlayerStatItem.kt b/composeApp/src/commonMain/kotlin/widget/PlayerStatItem.kt
index 44e5c81..cf07925 100644
--- a/composeApp/src/commonMain/kotlin/widget/PlayerStatItem.kt
+++ b/composeApp/src/commonMain/kotlin/widget/PlayerStatItem.kt
@@ -5,14 +5,13 @@ import androidx.compose.foundation.layout.Spacer
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 coil3.compose.AsyncImage
+import io.imrekaszab.eaplayers.theme.AppTheme
@Composable
fun PlayerStatItem(imageUrl: String, stat: String, label: String) {
@@ -20,18 +19,18 @@ fun PlayerStatItem(imageUrl: String, stat: String, label: String) {
AsyncImage(
model = imageUrl,
contentDescription = label,
- modifier = Modifier.size(32.dp)
+ modifier = Modifier.size(AppTheme.dimens.imageSize.playerStatItemIconSize)
)
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.tiny))
Text(
text = stat,
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onSurface
+ style = AppTheme.typography.body.large,
+ color = AppTheme.colors.yellow.default
)
Text(
text = label,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ style = AppTheme.typography.body.small,
+ color = AppTheme.colors.yellow.light1
)
}
}
@@ -42,19 +41,19 @@ fun PlayerStatItem(icon: ImageVector, stat: String, label: String) {
Icon(
imageVector = icon,
contentDescription = label,
- tint = MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(32.dp)
+ tint = AppTheme.colorScheme.primary,
+ modifier = Modifier.size(AppTheme.dimens.imageSize.playerStatItemIconSize)
)
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.tiny))
Text(
text = stat,
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onSurface
+ style = AppTheme.typography.body.large,
+ color = AppTheme.colorScheme.onSurface
)
Text(
text = label,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ style = AppTheme.typography.body.small,
+ color = AppTheme.colorScheme.onSurfaceVariant
)
}
}
diff --git a/composeApp/src/commonMain/kotlin/widget/PlayerStatRow.kt b/composeApp/src/commonMain/kotlin/widget/PlayerStatRow.kt
new file mode 100644
index 0000000..c8dc98e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/widget/PlayerStatRow.kt
@@ -0,0 +1,76 @@
+package widget
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import eaplayers.composeapp.generated.resources.Res
+import eaplayers.composeapp.generated.resources.height
+import eaplayers.composeapp.generated.resources.position
+import eaplayers.composeapp.generated.resources.rating
+import io.imrekaszab.eaplayers.domain.model.Player
+import io.imrekaszab.eaplayers.theme.AppTheme
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PlayerStatsRow(player: Player) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = AppTheme.dimens.margin.default),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ PlayerStatItem(
+ icon = Icons.Default.Star,
+ stat = player.overallRating.toString(),
+ label = stringResource(Res.string.rating)
+ )
+ PlayerStatItem(
+ icon = Icons.Default.Menu,
+ stat = player.position.shortLabel,
+ label = stringResource(Res.string.position)
+ )
+ PlayerStatItem(
+ icon = Icons.Default.KeyboardArrowUp,
+ stat = "${player.height} cm",
+ label = stringResource(Res.string.height)
+ )
+ }
+}
+
+@Composable
+fun PlayerStatRow(statItems: List) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(AppTheme.shapes.default.roundedDefault)
+ .background(
+ color = AppTheme.colors.blue.default,
+ shape = AppTheme.shapes.default.roundedDefault
+ )
+ .padding(AppTheme.dimens.margin.default),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ statItems.forEach { statItem ->
+ PlayerStatItem(
+ imageUrl = statItem.imageUrl,
+ stat = statItem.stat,
+ label = statItem.label
+ )
+ }
+ }
+}
+
+data class PlayerStat(
+ val imageUrl: String,
+ val stat: String,
+ val label: String
+)
diff --git a/composeApp/src/commonMain/kotlin/widget/TeamMateCard.kt b/composeApp/src/commonMain/kotlin/widget/TeamMateCard.kt
new file mode 100644
index 0000000..833e87c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/widget/TeamMateCard.kt
@@ -0,0 +1,85 @@
+package widget
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+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.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import coil3.compose.AsyncImage
+import eaplayers.composeapp.generated.resources.Res
+import eaplayers.composeapp.generated.resources.team_mates
+import io.imrekaszab.eaplayers.domain.model.Player
+import io.imrekaszab.eaplayers.theme.AppTheme
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun TeamMatesSection(player: Player, onTeamMateSelected: (Player) -> Unit) {
+ Text(
+ text = stringResource(Res.string.team_mates),
+ style = AppTheme.typography.heading.medium,
+ color = AppTheme.colorScheme.onBackground
+ )
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.tiny))
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(AppTheme.dimens.playerDetailView.boxSize)
+ ) {
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = AppTheme.dimens.playerDetailView.gridMinSize)
+ ) {
+ items(
+ player.teamMates.filter { it.id != player.id }
+ .sortedByDescending { it.overallRating }
+ ) { mate ->
+ TeamMateCard(mate = mate, onTeamMateSelected = onTeamMateSelected)
+ }
+ }
+ }
+}
+
+@Composable
+fun TeamMateCard(mate: Player, onTeamMateSelected: (Player) -> Unit) {
+ Column(
+ modifier = Modifier
+ .padding(AppTheme.dimens.margin.tiny)
+ .width(AppTheme.dimens.playerDetailView.cardWidth)
+ .clickable { onTeamMateSelected(mate) },
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .size(AppTheme.dimens.playerDetailView.imageSize)
+ .clip(AppTheme.shapes.default.circle),
+ model = mate.avatarUrl,
+ contentDescription = mate.lastName
+ )
+ Spacer(modifier = Modifier.height(AppTheme.dimens.margin.tiny))
+ Text(
+ text = mate.commonName ?: "${mate.firstName}. ${mate.lastName.first()}",
+ textAlign = TextAlign.Center,
+ style = AppTheme.typography.body.small.copy(fontWeight = FontWeight.Bold),
+ color = AppTheme.colorScheme.onSurface
+ )
+ Text(
+ text = mate.position.shortLabel,
+ style = AppTheme.typography.body.small,
+ color = AppTheme.colorScheme.onSurface
+ )
+ }
+}
diff --git a/composeApp/src/iosMain/kotlin/KoinIOS.kt b/composeApp/src/iosMain/kotlin/KoinIOS.kt
index 2c6ab0b..3b7ddb1 100644
--- a/composeApp/src/iosMain/kotlin/KoinIOS.kt
+++ b/composeApp/src/iosMain/kotlin/KoinIOS.kt
@@ -1,3 +1,4 @@
+@file:Suppress("MissingPackageDeclaration")
import co.touchlab.kermit.Logger
import io.imrekaszab.eaplayers.di.initKoin
import io.ktor.client.engine.darwin.Darwin
diff --git a/composeApp/src/iosMain/kotlin/MainViewController.kt b/composeApp/src/iosMain/kotlin/MainViewController.kt
index 792d978..ee6da0e 100644
--- a/composeApp/src/iosMain/kotlin/MainViewController.kt
+++ b/composeApp/src/iosMain/kotlin/MainViewController.kt
@@ -1,4 +1,4 @@
-@file:Suppress("MissingPackageDeclaration")
+@file:Suppress("MissingPackageDeclaration", "FunctionNaming")
import androidx.compose.ui.window.ComposeUIViewController
import navigation.Navigation
diff --git a/config/detekt.yml b/config/detekt.yml
index fc6b863..9b0b8c7 100644
--- a/config/detekt.yml
+++ b/config/detekt.yml
@@ -461,7 +461,6 @@ naming:
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
excludeClassPattern: '$^'
ignoreAnnotated: [ 'Composable' ]
- ignoreFunction: ['MainViewController']
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
@@ -602,7 +601,7 @@ style:
active: true
conversionFunctionPrefix: [ 'to' ]
DataClassShouldBeImmutable:
- active: true
+ active: false
DestructuringDeclarationWithTooManyEntries:
active: true
maxDestructuringEntries: 3
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b8585e2..e564691 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,9 +5,9 @@ javaTargetCompatibility = "17"
javaSourceCompatibility = "11"
agp = "8.5.2"
-kotlin = "2.0.0"
+kotlin = "2.0.10"
coroutines = "1.9.0-RC.2"
-compose = "1.7.0"
+compose = "1.7.3"
composeCompiler = "1.5.15"
compose-multiplatform = "1.7.0-alpha02"
compose-navigation = "2.8.0-alpha08"
@@ -27,7 +27,9 @@ koinAndroid = "3.5.0"
koinAndroidCompose = "3.5.0"
koinCompose = "1.1.1-RC1"
-detekt = "1.23.6"
+detekt = "1.23.7"
+composeMaterial = "1.4.0"
+statelyCommon = "2.0.5"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@@ -68,6 +70,8 @@ koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
+androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" }
+stately-common = { module = "co.touchlab:stately-common", version.ref = "statelyCommon" }
[plugins]
kotlin-parcelize = { id = "kotlin-parcelize", version = "kotlin" }
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index 2371b4e..1d114a7 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -9,7 +9,7 @@
/* Begin PBXBuildFile section */
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
- 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
+ 2152FB042600AC8F00CF470E /* EAPlayersApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* EAPlayersApp.swift */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
CF5D46FA2C6E8B28005EE48B /* KoinApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF5D46F92C6E8B28005EE48B /* KoinApplication.swift */; };
CF5D46FC2C6E8C38005EE48B /* LazyKoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF5D46FB2C6E8C38005EE48B /* LazyKoin.swift */; };
@@ -18,7 +18,7 @@
/* Begin PBXFileReference section */
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
- 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
+ 2152FB032600AC8F00CF470E /* EAPlayersApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EAPlayersApp.swift; sourceTree = ""; };
7555FF7B242A565900829871 /* EAPlayers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EAPlayers.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
@@ -78,7 +78,7 @@
058557BA273AAA24004C7B11 /* Assets.xcassets */,
7555FF82242A565900829871 /* ContentView.swift */,
7555FF8C242A565B00829871 /* Info.plist */,
- 2152FB032600AC8F00CF470E /* iOSApp.swift */,
+ 2152FB032600AC8F00CF470E /* EAPlayersApp.swift */,
058557D7273AAEEB004C7B11 /* Preview Content */,
);
path = iosApp;
@@ -199,7 +199,7 @@
buildActionMask = 2147483647;
files = (
CF5D46FA2C6E8B28005EE48B /* KoinApplication.swift in Sources */,
- 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
+ 2152FB042600AC8F00CF470E /* EAPlayersApp.swift in Sources */,
CF5D46FC2C6E8C38005EE48B /* LazyKoin.swift in Sources */,
7555FF83242A565900829871 /* ContentView.swift in Sources */,
);
diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift
index 3cd5c32..dc57478 100644
--- a/iosApp/iosApp/ContentView.swift
+++ b/iosApp/iosApp/ContentView.swift
@@ -16,6 +16,3 @@ struct ContentView: View {
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
}
}
-
-
-
diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/EAPlayersApp.swift
similarity index 59%
rename from iosApp/iosApp/iOSApp.swift
rename to iosApp/iosApp/EAPlayersApp.swift
index 7f49237..98aaf78 100644
--- a/iosApp/iosApp/iOSApp.swift
+++ b/iosApp/iosApp/EAPlayersApp.swift
@@ -2,13 +2,13 @@ import ComposeApp
import SwiftUI
@main
-struct iOSApp: App {
+struct EAPlayersApp: App {
// Lazy so it doesn't try to initialize before startKoin() is called
- lazy var logger = KoinApplication.getLogger(class: iOSApp.self)
-
+ lazy var logger = KoinApplication.getLogger(class: EAPlayersApp.self)
+
init() {
KoinApplication.start()
- logger.v(throwable: nil, tag: "Logger",message: { "App Started" })
+ logger.v(throwable: nil, tag: "Logger", message: { "App Started" })
}
var body: some Scene {
diff --git a/iosApp/iosApp/Koin/KoinApplication.swift b/iosApp/iosApp/Koin/KoinApplication.swift
index 2015260..0c426a7 100644
--- a/iosApp/iosApp/Koin/KoinApplication.swift
+++ b/iosApp/iosApp/Koin/KoinApplication.swift
@@ -40,4 +40,3 @@ extension KoinApplication {
return kotlinClass
}
}
-
diff --git a/modules/core/build.gradle.kts b/modules/core/build.gradle.kts
new file mode 100644
index 0000000..05b7759
--- /dev/null
+++ b/modules/core/build.gradle.kts
@@ -0,0 +1,59 @@
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidLibrary)
+ alias(libs.plugins.composeMultiplatform)
+}
+
+kotlin {
+ androidTarget {
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ }
+ }
+
+ iosX64()
+ iosArm64()
+ iosSimulatorArm64()
+
+ sourceSets {
+ androidMain.dependencies {
+ implementation(libs.kotlinx.coroutines.android)
+ }
+ commonMain.dependencies {
+ // Logging
+ implementation(libs.log.kermit)
+ implementation(libs.log.slf4j)
+
+ // Compose Multiplatform
+ implementation(libs.compose.viewmodel)
+ implementation(libs.compose.navigation)
+ implementation(libs.compose.multiplatform.ui)
+
+ implementation(libs.kotlinx.coroutines.core)
+ }
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ }
+ }
+}
+
+android {
+ namespace = "io.imrekaszab.eaplayers.core"
+
+ compileSdk = libs.versions.targetSdk.get().toInt()
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ dependencies {
+ // Detekt
+ detektPlugins(libs.detekt.formatting)
+ }
+}
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ActionCommand.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ActionCommand.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ActionCommand.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ActionCommand.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/BaseActionCommand.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/BaseActionCommand.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/BaseActionCommand.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/BaseActionCommand.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/Command.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/Command.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/Command.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/Command.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ParameterCommand.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ParameterCommand.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ParameterCommand.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/command/ParameterCommand.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/HttpException.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/HttpException.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/HttpException.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/HttpException.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/NetworkException.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/NetworkException.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/NetworkException.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/exception/NetworkException.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CommandUtil.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CommandUtil.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CommandUtil.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CommandUtil.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CoroutineUtil.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CoroutineUtil.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CoroutineUtil.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CoroutineUtil.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/FlowUtil.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/FlowUtil.kt
similarity index 95%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/FlowUtil.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/FlowUtil.kt
index 8f9310e..f5b5b20 100644
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/FlowUtil.kt
+++ b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/FlowUtil.kt
@@ -2,8 +2,8 @@ package io.imrekaszab.eaplayers.core.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.flow.Flow
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Lifecycle.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Lifecycle.kt
similarity index 93%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Lifecycle.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Lifecycle.kt
index 16367ba..f927cbb 100644
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Lifecycle.kt
+++ b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Lifecycle.kt
@@ -3,9 +3,9 @@ package io.imrekaszab.eaplayers.core.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/SnapshotState.kt b/modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/SnapshotState.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/SnapshotState.kt
rename to modules/core/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/SnapshotState.kt
diff --git a/modules/domain/build.gradle.kts b/modules/domain/build.gradle.kts
new file mode 100644
index 0000000..e81fbbf
--- /dev/null
+++ b/modules/domain/build.gradle.kts
@@ -0,0 +1,46 @@
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidLibrary)
+}
+
+kotlin {
+ androidTarget {
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ }
+ }
+
+ iosX64()
+ iosArm64()
+ iosSimulatorArm64()
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.kotlinx.coroutines.core)
+ }
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ }
+ }
+}
+
+android {
+ namespace = "io.imrekaszab.eaplayers.domain"
+
+ compileSdk = libs.versions.targetSdk.get().toInt()
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ dependencies {
+ // Detekt
+ detektPlugins(libs.detekt.formatting)
+ }
+}
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/action/EAPlayerAction.kt b/modules/domain/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/action/EAPlayerAction.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/action/EAPlayerAction.kt
rename to modules/domain/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/action/EAPlayerAction.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/Player.kt b/modules/domain/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/Player.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/Player.kt
rename to modules/domain/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/Player.kt
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/store/EAPlayerStore.kt b/modules/domain/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/store/EAPlayerStore.kt
similarity index 100%
rename from shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/store/EAPlayerStore.kt
rename to modules/domain/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/store/EAPlayerStore.kt
diff --git a/modules/theme/build.gradle.kts b/modules/theme/build.gradle.kts
new file mode 100644
index 0000000..187f205
--- /dev/null
+++ b/modules/theme/build.gradle.kts
@@ -0,0 +1,55 @@
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidLibrary)
+ alias(libs.plugins.jetbrainsCompose)
+ alias(libs.plugins.composeMultiplatform)
+}
+
+kotlin {
+ androidTarget {
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ }
+ }
+
+ iosX64()
+ iosArm64()
+ iosSimulatorArm64()
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.ui)
+ implementation(compose.components.resources)
+ implementation(compose.components.uiToolingPreview)
+ implementation(libs.compose.multiplatform.ui)
+ }
+ }
+}
+
+android {
+ namespace = "io.imrekaszab.eaplayers.theme"
+
+ compileSdk = libs.versions.targetSdk.get().toInt()
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+ sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ sourceSets["main"].res.srcDirs("src/androidMain/res")
+ sourceSets["main"].resources.srcDirs("src/commonMain/resources")
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ dependencies {
+ // Detekt
+ detektPlugins(libs.detekt.formatting)
+ }
+}
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-Black.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-Black.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-Black.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-Black.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-BlackItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-BlackItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-BlackItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-BlackItalic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-Bold.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-Bold.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-Bold.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-Bold.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-BoldItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-BoldItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-BoldItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-BoldItalic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-ExtraBold.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-ExtraBold.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-ExtraBold.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-ExtraBold.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-ExtraBoldItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-ExtraBoldItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-ExtraBoldItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-ExtraBoldItalic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-ExtraLight.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-ExtraLight.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-ExtraLight.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-ExtraLight.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-ExtraLightItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-ExtraLightItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-ExtraLightItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-ExtraLightItalic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-Italic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-Italic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-Italic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-Italic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-Light.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-Light.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-Light.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-Light.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-LightItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-LightItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-LightItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-LightItalic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-Medium.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-Medium.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-Medium.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-Medium.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-MediumItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-MediumItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-MediumItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-MediumItalic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-Regular.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-Regular.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-Regular.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-Regular.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-SemiBold.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-SemiBold.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-SemiBold.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-SemiBold.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-SemiBoldItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-SemiBoldItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-SemiBoldItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-SemiBoldItalic.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-Thin.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-Thin.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-Thin.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-Thin.ttf
diff --git a/composeApp/src/commonMain/composeResources/font/Poppins-ThinItalic.ttf b/modules/theme/src/commonMain/composeResources/font/Poppins-ThinItalic.ttf
similarity index 100%
rename from composeApp/src/commonMain/composeResources/font/Poppins-ThinItalic.ttf
rename to modules/theme/src/commonMain/composeResources/font/Poppins-ThinItalic.ttf
diff --git a/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppColors.kt b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppColors.kt
new file mode 100644
index 0000000..5f2de54
--- /dev/null
+++ b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppColors.kt
@@ -0,0 +1,281 @@
+package io.imrekaszab.eaplayers.theme
+
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.foundation.text.selection.TextSelectionColors
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.TextFieldColors
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+@Suppress("MagicNumber")
+@Immutable
+data class AppColors(
+ val primary: Color = Blue().default, // Use Blue as Primary
+ val onPrimary: Color = BlackAndWhite().white, // White on Blue
+ val primaryContainer: Color = Blue().light1, // Lighter Blue
+ val onPrimaryContainer: Color = BlackAndWhite().black, // Black on Light Blue
+ val secondary: Color = Yellow().default, // Use Yellow as Secondary
+ val onSecondary: Color = BlackAndWhite().black, // Black on Yellow
+ val secondaryContainer: Color = Yellow().light2, // Light Yellow
+ val onSecondaryContainer: Color = BlackAndWhite().black, // Black on Light Yellow
+ val backgroundSurfaceColors: BackgroundSurfaceColors = BackgroundSurfaceColors(),
+ val blackAndWhite: BlackAndWhite = BlackAndWhite(),
+ val red: Red = Red(),
+ val yellow: Yellow = Yellow(), // Yellow Color
+ val blue: Blue = Blue(), // Blue Color
+) {
+ // Define Yellow color variations
+ data class Yellow internal constructor(
+ val default: Color = Color(0xFFFFC107), // Default Yellow (Secondary)
+ val light1: Color = Color(0xFFFFD54F), // Lighter shade of Yellow
+ val light2: Color = Color(0xFFFFECB3), // Lighter yellow variant
+ val light3: Color = Color(0xFFFFF9C4) // Lightest shade of Yellow
+ )
+
+ // Define Blue color variations
+ data class Blue internal constructor(
+ val default: Color = Color(0xFF0D47A1), // Default Dark Blue (Primary)
+ val light1: Color = Color(0xFF5472D3), // Lighter Blue (PrimaryContainer)
+ val light2: Color = Color(0xFFBBDEFB), // Light Blue variant
+ val light3: Color = Color(0xFFE3F2FD) // Lightest Blue
+ )
+
+ // Define Red color variations
+ data class Red internal constructor(
+ val default: Color = Color(0xFFEE4338),
+ val light3: Color = Color(0xFFFFEFEE)
+ )
+
+ // Define Black and White color variations
+ data class BlackAndWhite internal constructor(
+ val black: Color = Color(0xFF000000),
+ val white: Color = Color(0xFFFFFFFF)
+ )
+
+ // Define Background and Surface colors for both Light and Dark themes
+ data class BackgroundSurfaceColors internal constructor(
+ val lightBackground: Color = Color(0xFFF6F6F6), // Light Background
+ val lightSurface: Color = Color.White, // Light Surface
+ val darkBackground: Color = Color(0xFF121212), // Dark Background
+ val darkSurface: Color = Color(0xFF1D1D1D) // Dark Surface
+ )
+
+ // BottomNavigation colors
+ data class BottomNavigation internal constructor(
+ val containerColor: Color = Color(0xFFFFFFFF),
+ val containerShadowColor: Color = Color(0xFF000000),
+ val defaultItem: ItemColor = ItemColor(contentColor = Color(0x80000000)),
+ val selectedItem: ItemColor = ItemColor(contentColor = Color(0xFF000000)),
+ val disabledItem: ItemColor = ItemColor(contentColor = Color(0x80EFEFEF))
+ ) {
+ data class ItemColor internal constructor(
+ val containerColor: Color = Color.Transparent,
+ val contentColor: Color = Color(0xFF000000)
+ )
+ }
+}
+
+internal fun getColorScheme(darkTheme: Boolean): ColorScheme =
+ if (darkTheme) {
+ getDarkColors(AppColors())
+ } else {
+ getLightColors(AppColors())
+ }
+
+// Define LightColors using the passed AppColors instance
+
+internal fun getLightColors(colors: AppColors) = lightColorScheme(
+ primary = colors.blue.default, // Use Blue as Primary
+ onPrimary = colors.blue.default, // Use Blue on Blue (for contrast elements on primary)
+ primaryContainer = colors.blue.light1, // Lighter Blue for Primary Container
+ onPrimaryContainer = colors.blackAndWhite.black, // Black on Light Blue
+ secondary = colors.yellow.default, // Use Yellow as Secondary
+ onSecondary = colors.blackAndWhite.black, // Black on Yellow
+ secondaryContainer = colors.yellow.light2, // Light Yellow for Secondary Container
+ onSecondaryContainer = colors.blackAndWhite.black, // Black on Light Yellow
+ background = colors.backgroundSurfaceColors.lightBackground, // Light Background (e.g., white)
+ onBackground = colors.blackAndWhite.black, // Black on Background
+ surface = colors.backgroundSurfaceColors.lightSurface, // Light Surface (e.g., white)
+ onSurface = colors.blackAndWhite.black, // Black on Surface (important for visibility)
+ error = colors.red.default, // Red for Error
+ onError = colors.blackAndWhite.white, // White on Red (for legibility)
+ inversePrimary = colors.blue.light2, // Lighter Blue for Inverse Primary
+ inverseSurface = colors.backgroundSurfaceColors.darkSurface, // Dark Surface for Inverse
+ inverseOnSurface = colors.blackAndWhite.white, // White on Dark Surface in Inverse
+ outline = colors.blackAndWhite.black, // Black for Outlines in Light Mode
+ outlineVariant = colors.blackAndWhite.black, // Black for Variant Outlines
+ scrim = colors.blackAndWhite.black // Black scrim for light mode
+)
+
+// Define DarkColors using the passed AppColors instance
+
+internal fun getDarkColors(colors: AppColors) = darkColorScheme(
+ primary = colors.blue.default, // Dark Blue as Primary
+ onPrimary = colors.blackAndWhite.white, // White on Dark Blue
+ primaryContainer = colors.blue.light2, // Lighter Blue for Primary Container
+ onPrimaryContainer = colors.blackAndWhite.white, // White on Lighter Blue
+ secondary = colors.yellow.default, // Yellow as Secondary
+ onSecondary = colors.blackAndWhite.black, // Black on Yellow
+ secondaryContainer = colors.yellow.light2, // Lighter Yellow for Secondary Container
+ onSecondaryContainer = colors.blackAndWhite.black, // Black on Light Yellow
+ background = colors.backgroundSurfaceColors.darkBackground, // Dark Background (e.g., dark gray)
+ onBackground = colors.blackAndWhite.white, // White on Dark Background
+ surface = colors.backgroundSurfaceColors.darkSurface, // Dark Surface (e.g., dark gray)
+ onSurface = colors.blackAndWhite.white, // White on Dark Surface
+ error = colors.red.default, // Red for Error
+ onError = colors.blackAndWhite.black, // Black on Red Error
+ inversePrimary = colors.blue.light1, // Light Blue for Inverse Primary
+ inverseSurface = colors.backgroundSurfaceColors.lightSurface, // Light Surface for Inverse
+ inverseOnSurface = colors.blackAndWhite.black, // Black on Light Surface in Inverse
+ outline = colors.blackAndWhite.white, // White for Outlines in Dark Mode
+ outlineVariant = colors.blackAndWhite.white, // White for Variant Outlines
+ scrim = colors.blackAndWhite.black // Black scrim for dark mode
+)
+
+/**
+ * A Material [ColorScheme] implementation which sets all colors to [debugColor] to discourage usage of
+ * [androidx.compose.material3.AppTheme.colorScheme] in preference to [AppTheme.colors].
+ */
+fun debugColors(
+ debugColor: Color = Color.Magenta
+) = lightColorScheme(
+ primary = debugColor,
+ onPrimary = debugColor,
+ primaryContainer = debugColor,
+ onPrimaryContainer = debugColor,
+ inversePrimary = debugColor,
+ secondary = debugColor,
+ onSecondary = debugColor,
+ secondaryContainer = debugColor,
+ onSecondaryContainer = debugColor,
+ tertiary = debugColor,
+ onTertiary = debugColor,
+ tertiaryContainer = debugColor,
+ onTertiaryContainer = debugColor,
+ background = debugColor,
+ onBackground = debugColor,
+ surface = debugColor,
+ onSurface = debugColor,
+ surfaceVariant = debugColor,
+ onSurfaceVariant = debugColor,
+ surfaceTint = debugColor,
+ inverseSurface = debugColor,
+ inverseOnSurface = debugColor,
+ error = debugColor,
+ onError = debugColor,
+ errorContainer = debugColor,
+ onErrorContainer = debugColor,
+ outline = debugColor,
+ outlineVariant = debugColor,
+ scrim = debugColor
+)
+
+@Composable
+fun defaultTextFieldColors(
+ focusedTextColor: Color = AppTheme.colorScheme.onSurface, // Text color when focused
+ unfocusedTextColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Text color when unfocused
+ disabledTextColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled text color
+ errorTextColor: Color = AppTheme.colorScheme.error, // Error text color
+
+ focusedContainerColor: Color = AppTheme.colorScheme.surface, // Background color when focused
+ unfocusedContainerColor: Color = AppTheme.colorScheme.surfaceVariant, // Background color when unfocused
+ disabledContainerColor: Color = AppTheme.colorScheme.surface.copy(alpha = 0.12f), // Disabled background
+ errorContainerColor: Color = AppTheme.colorScheme.errorContainer, // Background in error state
+
+ cursorColor: Color = AppTheme.colorScheme.primary, // Cursor color
+ errorCursorColor: Color = AppTheme.colorScheme.error, // Cursor color in error state
+
+ selectionColors: TextSelectionColors = LocalTextSelectionColors.current, // Text selection colors
+
+ focusedIndicatorColor: Color = AppTheme.colorScheme.primary, // Underline/Border color when focused
+ unfocusedIndicatorColor: Color = AppTheme.colorScheme.outline, // Underline/Border color when unfocused
+ disabledIndicatorColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled underline
+ errorIndicatorColor: Color = AppTheme.colorScheme.error, // Underline/Border color in error state
+
+ focusedLeadingIconColor: Color = AppTheme.colorScheme.onSurface, // Leading icon color when focused
+ unfocusedLeadingIconColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Leading icon color when unfocused
+ disabledLeadingIconColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled leading icon
+ errorLeadingIconColor: Color = AppTheme.colorScheme.error, // Leading icon color in error state
+
+ focusedTrailingIconColor: Color = AppTheme.colorScheme.onSurface, // Trailing icon color when focused
+ unfocusedTrailingIconColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Trailing icon color when unfocused
+ disabledTrailingIconColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled trailing icon
+ errorTrailingIconColor: Color = AppTheme.colorScheme.error, // Trailing icon color in error state
+
+ focusedLabelColor: Color = AppTheme.colorScheme.onSurface, // Label color when focused
+ unfocusedLabelColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Label color when unfocused
+ disabledLabelColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled label color
+ errorLabelColor: Color = AppTheme.colorScheme.error, // Label color in error state
+
+ focusedPlaceholderColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Placeholder color when focused
+ unfocusedPlaceholderColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Placeholder color when unfocused
+ disabledPlaceholderColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled placeholder
+ errorPlaceholderColor: Color = AppTheme.colorScheme.error, // Placeholder color in error state
+
+ focusedSupportingTextColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Supporting text when focused
+ unfocusedSupportingTextColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Supporting text when unfocused
+ disabledSupportingTextColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled supporting text
+ errorSupportingTextColor: Color = AppTheme.colorScheme.error, // Supporting text in error state
+
+ focusedPrefixColor: Color = AppTheme.colorScheme.onSurface, // Prefix color when focused
+ unfocusedPrefixColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Prefix color when unfocused
+ disabledPrefixColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled prefix
+ errorPrefixColor: Color = AppTheme.colorScheme.error, // Prefix color in error state
+
+ focusedSuffixColor: Color = AppTheme.colorScheme.onSurface, // Suffix color when focused
+ unfocusedSuffixColor: Color = AppTheme.colorScheme.onSurfaceVariant, // Suffix color when unfocused
+ disabledSuffixColor: Color = AppTheme.colorScheme.onSurface.copy(alpha = 0.38f), // Disabled suffix
+ errorSuffixColor: Color = AppTheme.colorScheme.error // Suffix color in error state
+): TextFieldColors = TextFieldDefaults.colors().copy(
+ focusedTextColor = focusedTextColor,
+ unfocusedTextColor = unfocusedTextColor,
+ disabledTextColor = disabledTextColor,
+ errorTextColor = errorTextColor,
+ focusedContainerColor = focusedContainerColor,
+ unfocusedContainerColor = unfocusedContainerColor,
+ disabledContainerColor = disabledContainerColor,
+ errorContainerColor = errorContainerColor,
+ cursorColor = cursorColor,
+ errorCursorColor = errorCursorColor,
+ textSelectionColors = selectionColors,
+ focusedIndicatorColor = focusedIndicatorColor,
+ unfocusedIndicatorColor = unfocusedIndicatorColor,
+ disabledIndicatorColor = disabledIndicatorColor,
+ errorIndicatorColor = errorIndicatorColor,
+ focusedLeadingIconColor = focusedLeadingIconColor,
+ unfocusedLeadingIconColor = unfocusedLeadingIconColor,
+ disabledLeadingIconColor = disabledLeadingIconColor,
+ errorLeadingIconColor = errorLeadingIconColor,
+ focusedTrailingIconColor = focusedTrailingIconColor,
+ unfocusedTrailingIconColor = unfocusedTrailingIconColor,
+ disabledTrailingIconColor = disabledTrailingIconColor,
+ errorTrailingIconColor = errorTrailingIconColor,
+ focusedLabelColor = focusedLabelColor,
+ unfocusedLabelColor = unfocusedLabelColor,
+ disabledLabelColor = disabledLabelColor,
+ errorLabelColor = errorLabelColor,
+ focusedPlaceholderColor = focusedPlaceholderColor,
+ unfocusedPlaceholderColor = unfocusedPlaceholderColor,
+ disabledPlaceholderColor = disabledPlaceholderColor,
+ errorPlaceholderColor = errorPlaceholderColor,
+ focusedSupportingTextColor = focusedSupportingTextColor,
+ unfocusedSupportingTextColor = unfocusedSupportingTextColor,
+ disabledSupportingTextColor = disabledSupportingTextColor,
+ errorSupportingTextColor = errorSupportingTextColor,
+ focusedPrefixColor = focusedPrefixColor,
+ unfocusedPrefixColor = unfocusedPrefixColor,
+ disabledPrefixColor = disabledPrefixColor,
+ errorPrefixColor = errorPrefixColor,
+ focusedSuffixColor = focusedSuffixColor,
+ unfocusedSuffixColor = unfocusedSuffixColor,
+ disabledSuffixColor = disabledSuffixColor,
+ errorSuffixColor = errorSuffixColor
+)
+
+// Your CompositionLocal for AppColors (adjust this if you already have it elsewhere)
+internal val LocalAppColors = staticCompositionLocalOf { AppColors() }
diff --git a/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppDimens.kt b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppDimens.kt
new file mode 100644
index 0000000..2155fc9
--- /dev/null
+++ b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppDimens.kt
@@ -0,0 +1,122 @@
+package io.imrekaszab.eaplayers.theme
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.jetbrains.compose.ui.tooling.preview.Preview
+
+@Immutable
+data class AppDimens internal constructor(
+ val margin: Margin = Margin(),
+ val fontSize: FontSize = FontSize(),
+ val lineHeight: LineHeight = LineHeight(),
+ val bottomNavigation: BottomNavigation = BottomNavigation(),
+ val divider: Divider = Divider(),
+ val drawerMenu: DrawerMenu = DrawerMenu(),
+ val textInput: TextInput = TextInput(),
+ val imageSize: ImageSize = ImageSize(),
+ val playerDetailView: PlayerDetailView = PlayerDetailView()
+) {
+ data class Margin internal constructor(
+ val huge: Dp = 56.dp,
+ val extraLarge: Dp = 48.dp,
+ val larger: Dp = 44.dp,
+ val large: Dp = 40.dp,
+ val bigger: Dp = 32.dp,
+ val bigRoomy: Dp = 28.dp,
+ val big: Dp = 24.dp,
+ val roomy: Dp = 20.dp,
+ val default: Dp = 16.dp,
+ val small: Dp = 12.dp,
+ val smaller: Dp = 10.dp,
+ val tiny: Dp = 8.dp,
+ val extraTiny: Dp = 4.dp,
+ val tiniest: Dp = 2.dp
+ )
+
+ data class FontSize internal constructor(
+ val headerL: TextUnit = 28.sp,
+ val headerM: TextUnit = 22.sp,
+ val headerS: TextUnit = 18.sp,
+ val headerXS: TextUnit = 18.sp,
+ val bodyL: TextUnit = 18.sp,
+ val bodyM: TextUnit = 16.sp,
+ val bodyS: TextUnit = 14.sp,
+ val bodyXS: TextUnit = 12.sp,
+ val buttonL: TextUnit = 20.sp,
+ val buttonM: TextUnit = 18.sp,
+ val buttonS: TextUnit = 16.sp,
+ val buttonT: TextUnit = 14.sp
+ )
+
+ data class LineHeight internal constructor(
+ val headerL: TextUnit = 34.sp,
+ val headerM: TextUnit = 28.sp,
+ val headerS: TextUnit = 23.sp,
+ val bodyL: TextUnit = 23.sp,
+ val buttonL: TextUnit = 50.sp,
+ val buttonM: TextUnit = 38.sp,
+ val buttonS: TextUnit = 32.sp,
+ val buttonT: TextUnit = 24.sp
+ )
+
+ data class Divider internal constructor(
+ val thickness: Dp = 1.dp
+ )
+
+ data class BottomNavigation internal constructor(
+ val shadowRadius: Dp = 24.dp,
+ val menuElevation: Dp = 3.dp
+ )
+
+ data class DrawerMenu internal constructor(
+ val dividerStartPadding: Dp = 60.dp
+ )
+
+ data class TextInput internal constructor(
+ val textInputMinHeight: Dp = 60.dp,
+ val textInputHeight: Dp = 104.dp,
+ val longTextInputHeight: Dp = 104.dp
+ )
+
+ data class ImageSize internal constructor(
+ val abilityItemImageSize: Dp = 40.dp,
+ val playerStatItemIconSize: Dp = 32.dp,
+ )
+
+ data class PlayerDetailView internal constructor(
+ val boxSize: Dp = 400.dp,
+ val imageSize: Dp = 50.dp,
+ val cardWidth: Dp = 80.dp,
+ val gridMinSize: Dp = 100.dp,
+ val playerItemImageSize: Dp = 100.dp
+ )
+}
+
+internal val LocalAppDimens = staticCompositionLocalOf { AppDimens() }
+
+@Preview
+@Composable
+private fun AppDimensPreview() {
+ AppTheme {
+ Column {
+ Box(
+ Modifier
+ .padding(AppTheme.dimens.margin.small)
+ .size(20.dp)
+ .background(Color.Magenta)
+ )
+ }
+ }
+}
diff --git a/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppRippleTheme.kt b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppRippleTheme.kt
new file mode 100644
index 0000000..6e46430
--- /dev/null
+++ b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppRippleTheme.kt
@@ -0,0 +1,30 @@
+package io.imrekaszab.eaplayers.theme
+
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.RippleConfiguration
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+object AppRippleTheme {
+ private val rippleAlpha = RippleAlpha(
+ pressedAlpha = 0.25f,
+ focusedAlpha = 0.25f,
+ draggedAlpha = 0.25f,
+ hoveredAlpha = 0.25f
+ )
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun getAppRippleTheme() = RippleConfiguration(
+ color = AppTheme.colors.blue.default,
+ rippleAlpha = rippleAlpha
+ )
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun getColoredRippleConfiguration(rippleColor: Color) = RippleConfiguration(
+ color = rippleColor,
+ rippleAlpha = rippleAlpha
+ )
+}
diff --git a/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppShapes.kt b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppShapes.kt
new file mode 100644
index 0000000..24e19f0
--- /dev/null
+++ b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppShapes.kt
@@ -0,0 +1,25 @@
+package io.imrekaszab.eaplayers.theme
+
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Shape
+
+@Immutable
+data class AppShapes(
+ private val appDimens: AppDimens,
+ val default: Default = Default(dimens = appDimens),
+) {
+ data class Default internal constructor(
+ private val dimens: AppDimens,
+ val circle: Shape = CircleShape,
+ val roundedBig: Shape = RoundedCornerShape(dimens.margin.big),
+ val roundedDefault: Shape = RoundedCornerShape(dimens.margin.default),
+ val roundedSmall: Shape = RoundedCornerShape(dimens.margin.small),
+ val roundedTiny: Shape = RoundedCornerShape(dimens.margin.tiny),
+ val roundedRoomy: Shape = RoundedCornerShape(dimens.margin.roomy)
+ )
+}
+
+internal val LocalAppShapes = staticCompositionLocalOf { error("No AppShapes provided") }
diff --git a/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTextSelectionColors.kt b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTextSelectionColors.kt
new file mode 100644
index 0000000..87ba45d
--- /dev/null
+++ b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTextSelectionColors.kt
@@ -0,0 +1,10 @@
+package io.imrekaszab.eaplayers.theme
+
+import androidx.compose.foundation.text.selection.TextSelectionColors
+import androidx.compose.runtime.Composable
+
+@Composable
+internal fun appTextSelectionColors() = TextSelectionColors(
+ handleColor = AppTheme.colors.blue.default,
+ backgroundColor = AppTheme.colors.blue.default.copy(alpha = 0.4F)
+)
diff --git a/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTheme.kt b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTheme.kt
new file mode 100644
index 0000000..c2b7bd5
--- /dev/null
+++ b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTheme.kt
@@ -0,0 +1,83 @@
+package io.imrekaszab.eaplayers.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalRippleConfiguration
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReadOnlyComposable
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AppTheme(
+ dimens: AppDimens = AppTheme.dimens,
+ shapes: AppShapes = AppShapes(AppTheme.dimens),
+ darkTheme: Boolean = isSystemInDarkTheme(), // Flag to toggle between light and dark themes
+ colors: AppColors = AppColors(),
+ colorScheme: ColorScheme = getColorScheme(darkTheme = darkTheme),
+ typography: AppTypography = AppTypography(
+ AppTheme.dimens,
+ colorScheme,
+ fontFamily = getDefaultFontFamily()
+ ),
+ content: @Composable () -> Unit
+) {
+ CompositionLocalProvider(
+ LocalAppColors provides colors,
+ LocalAppDimens provides dimens,
+ LocalAppTypography provides typography,
+ LocalAppShapes provides shapes,
+ LocalRippleConfiguration provides AppRippleTheme.getAppRippleTheme(),
+ ) {
+ MaterialTheme(colorScheme = debugColors()) {
+ CompositionLocalProvider(
+ LocalTextStyle provides AppTheme.typography.body.medium,
+ LocalRippleConfiguration provides AppRippleTheme.getAppRippleTheme(),
+ LocalAppShapes provides AppShapes(dimens),
+ LocalTextSelectionColors provides appTextSelectionColors()
+ ) {
+ content()
+ }
+ }
+ }
+}
+
+object AppTheme {
+ val colors: AppColors
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalAppColors.current
+
+ val dimens: AppDimens
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalAppDimens.current
+
+ val typography: AppTypography
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalAppTypography.current
+
+ // Dynamically generate the ColorScheme from the current AppColors
+ val colorScheme: ColorScheme
+ @Composable
+ @ReadOnlyComposable
+ get() {
+ val darkTheme = isSystemInDarkTheme()
+ return if (darkTheme) {
+ getDarkColors(LocalAppColors.current)
+ } else {
+ getLightColors(
+ LocalAppColors.current
+ )
+ }
+ }
+ val shapes: AppShapes
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalAppShapes.current
+}
diff --git a/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTypography.kt b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTypography.kt
new file mode 100644
index 0000000..a5f8095
--- /dev/null
+++ b/modules/theme/src/commonMain/kotlin/io/imrekaszab/eaplayers/theme/AppTypography.kt
@@ -0,0 +1,135 @@
+package io.imrekaszab.eaplayers.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.em
+import eaplayers.modules.theme.generated.resources.Poppins_Black
+import eaplayers.modules.theme.generated.resources.Poppins_Bold
+import eaplayers.modules.theme.generated.resources.Poppins_Light
+import eaplayers.modules.theme.generated.resources.Poppins_Medium
+import eaplayers.modules.theme.generated.resources.Poppins_Regular
+import eaplayers.modules.theme.generated.resources.Poppins_SemiBold
+import eaplayers.modules.theme.generated.resources.Res
+import org.jetbrains.compose.resources.Font
+
+@Composable
+fun getDefaultFontFamily(): FontFamily =
+ FontFamily(
+ Font(Res.font.Poppins_Regular, FontWeight.Normal),
+ Font(Res.font.Poppins_Medium, FontWeight.Medium),
+ Font(Res.font.Poppins_Light, FontWeight.Light),
+ Font(Res.font.Poppins_SemiBold, FontWeight.SemiBold),
+ Font(Res.font.Poppins_Bold, FontWeight.Bold),
+ Font(Res.font.Poppins_Black, FontWeight.Black)
+ )
+
+@Immutable
+data class AppTypography(
+ val dimens: AppDimens,
+ val scheme: ColorScheme,
+ val fontFamily: FontFamily,
+ val heading: Heading = Heading(dimens, scheme, fontFamily),
+ val body: Body = Body(dimens, scheme, fontFamily),
+ val button: Button = Button(dimens, fontFamily)
+)
+
+data class Heading internal constructor(
+ private val dimens: AppDimens,
+ val scheme: ColorScheme,
+ private val fontFamily: FontFamily,
+ val large: TextStyle = TextStyle(
+ fontFamily = fontFamily,
+ fontSize = dimens.fontSize.headerL,
+ fontWeight = FontWeight.Bold,
+ lineHeight = dimens.lineHeight.headerL,
+ color = scheme.onPrimary
+ ),
+ val medium: TextStyle = large.copy(
+ fontSize = dimens.fontSize.headerM,
+ lineHeight = dimens.lineHeight.headerM
+ ),
+ val small: TextStyle = large.copy(
+ fontSize = dimens.fontSize.headerS,
+ lineHeight = dimens.lineHeight.headerS
+ ),
+ val extraSmall: TextStyle = TextStyle(
+ fontFamily = fontFamily,
+ fontSize = dimens.fontSize.headerXS,
+ fontWeight = FontWeight.W500,
+ color = scheme.onPrimary
+ )
+)
+
+data class Body internal constructor(
+ private val dimens: AppDimens,
+ val scheme: ColorScheme,
+ private val fontFamily: FontFamily,
+ val large: TextStyle = TextStyle(
+ fontFamily = fontFamily,
+ fontSize = dimens.fontSize.bodyL,
+ lineHeight = dimens.lineHeight.bodyL,
+ fontWeight = FontWeight.W500,
+ color = scheme.onPrimary
+ ),
+ val medium: TextStyle = TextStyle(
+ fontFamily = fontFamily,
+ fontSize = dimens.fontSize.bodyM,
+ fontWeight = FontWeight.W500,
+ color = scheme.onPrimary
+ ),
+ val small: TextStyle = medium.copy(
+ fontSize = dimens.fontSize.bodyS,
+ fontWeight = FontWeight.Normal
+ ),
+ val extraSmall: TextStyle = medium.copy(
+ fontSize = dimens.fontSize.bodyXS,
+ ),
+)
+
+data class Button internal constructor(
+ private val dimens: AppDimens,
+ private val fontFamily: FontFamily,
+ val large: TextStyle = TextStyle(
+ fontFamily = fontFamily,
+ fontSize = dimens.fontSize.buttonL,
+ fontWeight = FontWeight.W500,
+ lineHeight = dimens.lineHeight.buttonL,
+ letterSpacing = (-0.01).em,
+ ),
+ val medium: TextStyle = large.copy(
+ fontSize = dimens.fontSize.buttonM,
+ lineHeight = dimens.lineHeight.buttonM,
+ ),
+ val small: TextStyle = large.copy(
+ fontSize = dimens.fontSize.buttonS,
+ lineHeight = dimens.lineHeight.buttonS,
+ ),
+ val tiny: TextStyle = large.copy(
+ fontSize = dimens.fontSize.buttonT,
+ lineHeight = dimens.lineHeight.buttonT,
+ )
+)
+
+@Composable
+fun ProvideAppTypography(
+ dimens: AppDimens,
+ scheme: ColorScheme,
+ content: @Composable () -> Unit
+) {
+ val typography = AppTypography(
+ dimens = dimens,
+ scheme = scheme,
+ fontFamily = getDefaultFontFamily()
+ )
+
+ CompositionLocalProvider(LocalAppTypography provides typography, content = content)
+}
+
+internal val LocalAppTypography =
+ staticCompositionLocalOf { error("No AppTypography provided") }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6a8b388..3841244 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -25,4 +25,7 @@ dependencyResolutionManagement {
rootProject.name = "EAPlayers"
include(":composeApp")
-include(":shared")
\ No newline at end of file
+include(":shared")
+include(":modules:core")
+include(":modules:domain")
+include(":modules:theme")
\ No newline at end of file
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index cd15814..f6d6be0 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -1,3 +1,6 @@
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
@@ -8,10 +11,9 @@ plugins {
kotlin {
androidTarget {
- compilations.all {
- kotlinOptions {
- jvmTarget = "1.8"
- }
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
}
}
@@ -58,6 +60,10 @@ kotlin {
implementation(libs.ktor.content.negotiation)
implementation(libs.ktor.serialization)
implementation(libs.kotlinx.coroutines.core)
+
+ // Modules
+ implementation(projects.modules.core)
+ implementation(projects.modules.domain)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CharSequence.kt b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CharSequence.kt
deleted file mode 100644
index b600f1d..0000000
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/CharSequence.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package io.imrekaszab.eaplayers.core.util
-
-fun CharSequence.keepMatching(regex: Regex): CharSequence {
- val matchResult = regex.find(this)
- val values = matchResult?.groupValues
- return values?.joinToString().orEmpty()
-}
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Interpolation.kt b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Interpolation.kt
deleted file mode 100644
index fa7d30c..0000000
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/Interpolation.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.imrekaszab.eaplayers.core.util
-
-import androidx.annotation.FloatRange
-
-/**
- * Linearly interpolate between two values
- */
-fun lerp(
- startValue: Float,
- endValue: Float,
- @FloatRange(from = 0.0, to = 1.0) fraction: Float
-) = startValue + fraction * (endValue - startValue)
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/NumberUtil.kt b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/NumberUtil.kt
deleted file mode 100644
index 67b006e..0000000
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/NumberUtil.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package io.imrekaszab.eaplayers.core.util
-
-fun Int.isEven() = this % 2 == 0
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/ViewModelStoreExt.kt b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/ViewModelStoreExt.kt
deleted file mode 100644
index d4b5512..0000000
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/core/util/ViewModelStoreExt.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-@file:Suppress("PackageDirectoryMismatch", "InvalidPackageDeclaration")
-
-package androidx.lifecycle
-
-@Suppress("RestrictedApi")
-fun ViewModelStore.getAll() = keys().map { get(it) }.toSet()
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/PlayerMappers.kt b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/PlayerMappers.kt
index 316dc2c..0a0814d 100644
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/PlayerMappers.kt
+++ b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/domain/model/PlayerMappers.kt
@@ -1,6 +1,11 @@
package io.imrekaszab.eaplayers.domain.model
-import io.imrekaszab.eaplayers.model.*
+import io.imrekaszab.eaplayers.model.AbilityTypeApiModel
+import io.imrekaszab.eaplayers.model.NationalityApiModel
+import io.imrekaszab.eaplayers.model.PlayerAbilityApiModel
+import io.imrekaszab.eaplayers.model.PlayerApiModel
+import io.imrekaszab.eaplayers.model.PositionApiModel
+import io.imrekaszab.eaplayers.model.TeamApiModel
fun PlayerApiModel.toPlayer() = Player(
id = this.id,
diff --git a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/network/PlayerApiImpl.kt b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/network/PlayerApiImpl.kt
index d0bd649..a0c9e09 100644
--- a/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/network/PlayerApiImpl.kt
+++ b/shared/src/commonMain/kotlin/io/imrekaszab/eaplayers/network/PlayerApiImpl.kt
@@ -6,10 +6,12 @@ import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
+private const val LIMIT = 100
+private const val URL_STRING = "rating/fc-24"
class PlayerApiImpl(private val httpClient: HttpClient) : PlayerApi {
override suspend fun getPlayersResponse(search: String): PlayersResponse? {
- val request = httpClient.get("rating/fc-24") {
- parameter("limit", 100)
+ val request = httpClient.get(URL_STRING) {
+ parameter("limit", LIMIT)
if (search.isNotEmpty()) {
parameter("search", search)
}
@@ -23,8 +25,8 @@ class PlayerApiImpl(private val httpClient: HttpClient) : PlayerApi {
}
override suspend fun getPlayersResponseByTeam(teamId: Int): PlayersResponse? {
- val request = httpClient.get("rating/fc-24") {
- parameter("limit", 100)
+ val request = httpClient.get(URL_STRING) {
+ parameter("limit", LIMIT)
parameter("team", teamId)
}