diff --git a/README.md b/README.md index 76ae7da6e..241039ee9 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ As Firebase Analytics does not yet support Kotlin Multiplatform, the implementat | :core:ui | Done | ✔️ | ✔️ | ✔️ | ❌ | | :feature:bookmarks | In progress | ✔️ | ✔️ | ✔️ | ❌ | | :feature:foryou | In progress | ✔️ | ✔️ | ✔️ | ❌ | -| :feature:interests | Not started | ❌ | ❌ | ❌ | ❌ | +| :feature:interests | In progress | ✔️ | ✔️ | ✔️ | ❌ | | :feature:search | Not started | ❌ | ❌ | ❌ | ❌ | | :feature:settings | Not started | ❌ | ❌ | ❌ | ❌ | | :feature:topic | Not started | ❌ | ❌ | ❌ | ❌ | diff --git a/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/InterestsItem.kt b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/InterestsItem.kt new file mode 100644 index 000000000..32448628c --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/InterestsItem.kt @@ -0,0 +1,212 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import coil3.ImageLoader +import coil3.compose.LocalPlatformContext +import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage +import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton +import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons +import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme +import nowinandroid.core.ui.generated.resources.Res +import nowinandroid.core.ui.generated.resources.core_ui_interests_card_follow_button_content_desc +import nowinandroid.core.ui.generated.resources.core_ui_interests_card_unfollow_button_content_desc +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun InterestsItem( + name: String, + following: Boolean, + topicImageUrl: String, + onClick: () -> Unit, + onFollowButtonClick: (Boolean) -> Unit, + modifier: Modifier = Modifier, + iconModifier: Modifier = Modifier, + description: String = "", + isSelected: Boolean = false, +) { + ListItem( + leadingContent = { + InterestsIcon(topicImageUrl, iconModifier.size(64.dp)) + }, + headlineContent = { + Text(text = name) + }, + supportingContent = { + Text(text = description) + }, + trailingContent = { + NiaIconToggleButton( + checked = following, + onCheckedChange = onFollowButtonClick, + icon = { + Icon( + imageVector = NiaIcons.Add, + contentDescription = stringResource( + Res.string.core_ui_interests_card_follow_button_content_desc, + ), + ) + }, + checkedIcon = { + Icon( + imageVector = NiaIcons.Check, + contentDescription = stringResource( + Res.string.core_ui_interests_card_unfollow_button_content_desc, + ), + ) + }, + ) + }, + colors = ListItemDefaults.colors( + containerColor = if (isSelected) { + MaterialTheme.colorScheme.surfaceVariant + } else { + Color.Transparent + }, + ), + modifier = modifier + .semantics(mergeDescendants = true) { + selected = isSelected + } + .clickable(enabled = true, onClick = onClick), + ) +} + +@Composable +private fun InterestsIcon(topicImageUrl: String, modifier: Modifier = Modifier) { + if (topicImageUrl.isEmpty()) { + Icon( + modifier = modifier + .background(MaterialTheme.colorScheme.surface) + .padding(4.dp), + imageVector = NiaIcons.Person, + // decorative image + contentDescription = null, + ) + } else { + DynamicAsyncImage( + imageUrl = topicImageUrl, + contentDescription = null, + modifier = modifier, + imageLoader = ImageLoader(LocalPlatformContext.current), + ) + } +} + +@Preview +@Composable +private fun InterestsCardPreview() { + NiaTheme { + Surface { + InterestsItem( + name = "Compose", + description = "Description", + following = false, + topicImageUrl = "", + onClick = { }, + onFollowButtonClick = { }, + ) + } + } +} + +@Preview +@Composable +private fun InterestsCardLongNamePreview() { + NiaTheme { + Surface { + InterestsItem( + name = "This is a very very very very long name", + description = "Description", + following = true, + topicImageUrl = "", + onClick = { }, + onFollowButtonClick = { }, + ) + } + } +} + +@Preview +@Composable +private fun InterestsCardLongDescriptionPreview() { + NiaTheme { + Surface { + InterestsItem( + name = "Compose", + description = "This is a very very very very very very very " + + "very very very long description", + following = false, + topicImageUrl = "", + onClick = { }, + onFollowButtonClick = { }, + ) + } + } +} + +@Preview +@Composable +private fun InterestsCardWithEmptyDescriptionPreview() { + NiaTheme { + Surface { + InterestsItem( + name = "Compose", + description = "", + following = true, + topicImageUrl = "", + onClick = { }, + onFollowButtonClick = { }, + ) + } + } +} + +@Preview +@Composable +private fun InterestsCardSelectedPreview() { + NiaTheme { + Surface { + InterestsItem( + name = "Compose", + description = "", + following = true, + topicImageUrl = "", + onClick = { }, + onFollowButtonClick = { }, + isSelected = true, + ) + } + } +} diff --git a/feature/interests/build.gradle.kts b/feature/interests/build.gradle.kts index ee6aaf122..a97a304df 100644 --- a/feature/interests/build.gradle.kts +++ b/feature/interests/build.gradle.kts @@ -15,19 +15,38 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) - alias(libs.plugins.nowinandroid.android.library.compose) + alias(libs.plugins.nowinandroid.cmp.feature) + alias(libs.plugins.jetbrains.compose) + alias(libs.plugins.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) + alias(libs.plugins.roborazzi) } android { namespace = "com.google.samples.apps.nowinandroid.feature.interests" } -dependencies { - implementation(projects.core.data) - implementation(projects.core.domain) - - testImplementation(projects.core.testing) - - androidTestImplementation(projects.core.testing) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(projects.core.data) + implementation(projects.core.domain) + implementation(projects.core.ui) + implementation(compose.material3) + implementation(compose.foundation) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + } + commonMain.dependencies { + implementation(projects.core.testing) + } + androidUnitTest.dependencies { + implementation(libs.robolectric) + implementation(libs.roborazzi) + implementation(projects.core.screenshotTesting) + } + androidInstrumentedTest.dependencies { + implementation(projects.core.testing) + } + } } diff --git a/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt b/feature/interests/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt similarity index 100% rename from feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt rename to feature/interests/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt diff --git a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/src/androidUnitTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt similarity index 100% rename from feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt rename to feature/interests/src/androidUnitTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt diff --git a/feature/interests/src/main/res/values/strings.xml b/feature/interests/src/commonMain/composeResources/values/strings.xml similarity index 100% rename from feature/interests/src/main/res/values/strings.xml rename to feature/interests/src/commonMain/composeResources/values/strings.xml diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt similarity index 86% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt rename to feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index 468550878..f3e570709 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -22,10 +22,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -33,13 +29,19 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent +import com.google.samples.apps.nowinandroid.core.ui.collectAsStateWithLifecycle +import nowinandroid.feature.interests.generated.resources.Res +import nowinandroid.feature.interests.generated.resources.feature_interests_empty_header +import nowinandroid.feature.interests.generated.resources.feature_interests_loading +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.PreviewParameter @Composable fun InterestsRoute( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, highlightSelectedTopic: Boolean = false, - viewModel: InterestsViewModel = hiltViewModel(), + viewModel: InterestsViewModel, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -71,7 +73,7 @@ internal fun InterestsScreen( InterestsUiState.Loading -> NiaLoadingWheel( modifier = modifier, - contentDesc = stringResource(id = R.string.feature_interests_loading), + contentDesc = stringResource(Res.string.feature_interests_loading), ) is InterestsUiState.Interests -> @@ -92,7 +94,7 @@ internal fun InterestsScreen( @Composable private fun InterestsEmptyScreen() { - Text(text = stringResource(id = R.string.feature_interests_empty_header)) + Text(text = stringResource(Res.string.feature_interests_empty_header)) } @DevicePreviews diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt similarity index 96% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt rename to feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index b369ac5ab..22dc4319c 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -24,15 +24,13 @@ import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCa import com.google.samples.apps.nowinandroid.core.domain.TopicSortField import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject +import me.tatarka.inject.annotations.Inject -@HiltViewModel class InterestsViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, val userDataRepository: UserDataRepository, diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt similarity index 100% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt rename to feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt b/feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt similarity index 93% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt rename to feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt index 8a0f2d130..3a9626437 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt +++ b/feature/interests/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt @@ -22,7 +22,6 @@ import androidx.navigation.NavOptions import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument -import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute const val TOPIC_ID_ARG = "topicId" const val INTERESTS_ROUTE_BASE = "interests_route" @@ -50,6 +49,6 @@ fun NavGraphBuilder.interestsScreen( }, ), ) { - InterestsRoute(onTopicClick = onTopicClick) +// InterestsRoute(onTopicClick = onTopicClick) } } diff --git a/feature/interests/src/main/AndroidManifest.xml b/feature/interests/src/main/AndroidManifest.xml deleted file mode 100644 index 51d0cfc2e..000000000 --- a/feature/interests/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file