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) }