diff --git a/app-ios/Sources/ContributorFeature/ContributorsCountItem.swift b/app-ios/Sources/ContributorFeature/ContributorsCountItem.swift new file mode 100644 index 000000000..5d7948b23 --- /dev/null +++ b/app-ios/Sources/ContributorFeature/ContributorsCountItem.swift @@ -0,0 +1,49 @@ +import SwiftUI +import Theme + +struct ContributorsCountItem: View { + let totalContributor: Int + let duration: Double = 1.0 + @State private var tracker = 0 + + var body: some View { + VStack(alignment: .leading) { + + Text(String(localized: "Total", bundle: .module)) + .textStyle(.titleMedium) + .foregroundStyle(AssetColors.Surface.onSurfaceVariant.swiftUIColor) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack { + Text("\(tracker)") + .textStyle(.headlineLarge) + .foregroundStyle(AssetColors.Surface.onSurface.swiftUIColor) + .onAppear() { + animate() + } + + Text(String(localized: "Person", bundle: .module)) + .textStyle(.headlineSmall) + .foregroundStyle(AssetColors.Surface.onSurface.swiftUIColor) + } + + Spacer() + .frame(height: 16) + Divider() + } + } + + func animate() { + let interval = duration / Double(totalContributor) + Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { time in + tracker += 1 + if tracker == totalContributor { + time.invalidate() + } + } + } +} + +#Preview { + ContributorsCountItem(totalContributor: 112) +} diff --git a/app-ios/Sources/ContributorFeature/KmpPresenterContributorView.swift b/app-ios/Sources/ContributorFeature/KmpPresenterContributorView.swift index 46ef56ee2..4352b1f0a 100644 --- a/app-ios/Sources/ContributorFeature/KmpPresenterContributorView.swift +++ b/app-ios/Sources/ContributorFeature/KmpPresenterContributorView.swift @@ -27,6 +27,12 @@ struct KmpPresenterContributorView: View { if let existsState = state as? Exists { ScrollView { LazyVStack(spacing: 0) { + + ContributorsCountItem(totalContributor: existsState.contributors.count) + .frame(maxWidth: .infinity) + .padding(.horizontal, 16) + .padding(.vertical, 10) + ForEach(existsState.contributors, id: \.id) { value in let contributor = Model.Contributor( id: Int(value.id), diff --git a/app-ios/Sources/ContributorFeature/Resources/Localizable.xcstrings b/app-ios/Sources/ContributorFeature/Resources/Localizable.xcstrings index b2ad6f9c7..e7f58f053 100644 --- a/app-ios/Sources/ContributorFeature/Resources/Localizable.xcstrings +++ b/app-ios/Sources/ContributorFeature/Resources/Localizable.xcstrings @@ -16,7 +16,39 @@ } } } + }, + "Person" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "persons" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "人" + } + } + } + }, + "Total" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "合計" + } + } + } } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/app-ios/Sources/ContributorFeature/SwiftUIContributorView.swift b/app-ios/Sources/ContributorFeature/SwiftUIContributorView.swift index 115183940..6a46d977c 100644 --- a/app-ios/Sources/ContributorFeature/SwiftUIContributorView.swift +++ b/app-ios/Sources/ContributorFeature/SwiftUIContributorView.swift @@ -14,6 +14,12 @@ struct SwiftUIContributorView: View { if let contributors = store.contributors { ScrollView { LazyVStack(spacing: 0) { + + ContributorsCountItem(totalContributor: contributors.count) + .frame(maxWidth: .infinity) + .padding(.horizontal, 16) + .padding(.vertical, 10) + ForEach(contributors, id: \.id) { contributor in ContributorListItemView(contributor: contributor) { url in store.send(.view(.contributorButtonTapped(url))) diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/ContributorsScreenRobot.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/ContributorsScreenRobot.kt index 97177145a..087574c02 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/ContributorsScreenRobot.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/ContributorsScreenRobot.kt @@ -5,10 +5,12 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.performScrollToIndex import io.github.droidkaigi.confsched.contributors.ContributorsItemTestTagPrefix import io.github.droidkaigi.confsched.contributors.ContributorsScreen import io.github.droidkaigi.confsched.contributors.ContributorsTestTag +import io.github.droidkaigi.confsched.contributors.ContributorsTotalCountTestTag import io.github.droidkaigi.confsched.contributors.component.ContributorsItemImageTestTagPrefix import io.github.droidkaigi.confsched.contributors.component.ContributorsUserNameTextTestTagPrefix import io.github.droidkaigi.confsched.droidkaigiui.Inject @@ -80,6 +82,14 @@ class ContributorsScreenRobot @Inject constructor( .assertCountAtLeast(2) } + fun checkContributorTotalCountDisplayed() { + // Check contributors total count is being displayed + composeTestRule + .onNode(hasTestTag(ContributorsTotalCountTestTag)) + .assertExists() + .isDisplayed() + } + fun checkDoesNotFirstContributorItemDisplayed() { val contributor = Contributor.fakes().first() composeTestRule diff --git a/feature/contributors/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreenTest.kt b/feature/contributors/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreenTest.kt index 14b9d77d2..be89a0665 100644 --- a/feature/contributors/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreenTest.kt +++ b/feature/contributors/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreenTest.kt @@ -50,6 +50,11 @@ class ContributorsScreenTest(private val testCase: DescribedBehavior コントリビューター + 合計 + diff --git a/feature/contributors/src/commonMain/composeResources/values/strings.xml b/feature/contributors/src/commonMain/composeResources/values/strings.xml index 5ac45c4ff..71bd9eb6a 100644 --- a/feature/contributors/src/commonMain/composeResources/values/strings.xml +++ b/feature/contributors/src/commonMain/composeResources/values/strings.xml @@ -1,4 +1,6 @@ Contributor + Total + persons diff --git a/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreen.kt b/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreen.kt index dd923ecea..34d8ebc77 100644 --- a/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreen.kt +++ b/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched/contributors/ContributorsScreen.kt @@ -24,6 +24,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import conference_app_2024.feature.contributors.generated.resources.contributor_title import io.github.droidkaigi.confsched.compose.rememberEventFlow +import io.github.droidkaigi.confsched.contributors.component.ContributorsCountItem import io.github.droidkaigi.confsched.contributors.component.ContributorsItem import io.github.droidkaigi.confsched.droidkaigiui.SnackbarMessageEffect import io.github.droidkaigi.confsched.droidkaigiui.UserMessageStateHolder @@ -36,6 +37,7 @@ const val contributorsScreenRoute = "contributors" const val ContributorsScreenTestTag = "ContributorsScreenTestTag" const val ContributorsTestTag = "ContributorsTestTag" const val ContributorsItemTestTagPrefix = "ContributorsItemTestTag:" +const val ContributorsTotalCountTestTag = "ContributorsTotalCountTestTag" fun NavGraphBuilder.contributorsScreens( onNavigationIconClick: () -> Unit, @@ -161,6 +163,14 @@ private fun Contributors( modifier = modifier.testTag(ContributorsTestTag), contentPadding = contentPadding, ) { + item { + ContributorsCountItem( + totalContributor = contributors.size, + modifier = Modifier + .fillMaxWidth() + .testTag(ContributorsTotalCountTestTag), + ) + } items(contributors) { ContributorsItem( contributor = it, diff --git a/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched/contributors/component/ContributorsCountItem.kt b/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched/contributors/component/ContributorsCountItem.kt new file mode 100644 index 000000000..33f41b2d6 --- /dev/null +++ b/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched/contributors/component/ContributorsCountItem.kt @@ -0,0 +1,70 @@ +package io.github.droidkaigi.confsched.contributors.component + +import androidx.compose.animation.core.EaseOutQuart +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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 conference_app_2024.feature.contributors.generated.resources.contributor_person +import conference_app_2024.feature.contributors.generated.resources.contributor_total +import io.github.droidkaigi.confsched.contributors.ContributorsRes +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun ContributorsCountItem( + totalContributor: Int, + modifier: Modifier = Modifier, +) { + var targetValue by remember { mutableStateOf(0) } + val animatedTotalContributor by animateIntAsState( + targetValue = targetValue, + animationSpec = tween( + delayMillis = 300, + durationMillis = 1000, + easing = EaseOutQuart, + ), + ) + LaunchedEffect(totalContributor) { + targetValue = totalContributor + } + Column( + modifier = modifier + .padding(horizontal = 16.dp, vertical = 10.dp), + ) { + Text( + text = stringResource(ContributorsRes.string.contributor_total), + style = MaterialTheme.typography.titleMedium, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.Bottom, + ) { + Text( + text = "$animatedTotalContributor", + style = MaterialTheme.typography.headlineLarge, + ) + Text( + text = stringResource(ContributorsRes.string.contributor_person), + style = MaterialTheme.typography.headlineSmall, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + } +}