From d94fe4ec37f7b67a6a1a825d3ef9d27771f975d3 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sat, 15 Jun 2024 18:10:25 +0900 Subject: [PATCH 1/2] Add empty eventmap module --- .../data/eventmap/EventMapApiModule.kt | 15 ++ .../data/eventmap/EventMapRepositoryModule.kt | 34 ++++ .../eventmap/DefaultEventMapRepository.kt | 36 ++++ .../data/eventmap/EventMapApiClient.kt | 21 +++ .../data/eventmap/FakeEventMapApiClient.kt | 33 ++++ .../eventmap/response/EventMapResponse.kt | 3 + .../confsched/model/EventMapEvent.kt | 3 + .../confsched/model/EventMapRepository.kt | 18 ++ .../confsched/testing/MiniRobots.kt | 24 +++ feature/eventmap/.gitignore | 1 + feature/eventmap/build.gradle.kts | 23 +++ feature/eventmap/proguard-rules.pro | 21 +++ .../confsched/eventmap/EventMapScreenTest.kt | 66 +++++++ .../resources/robolectric.properties | 7 + .../confsched/eventmap/EventMapScreen.kt | 165 ++++++++++++++++++ .../eventmap/EventMapScreenPresenter.kt | 28 +++ .../eventmap/component/EventMapItem.kt | 39 +++++ .../confsched/eventmap/IosCompose.kt | 31 ++++ gradle.properties | 2 +- settings.gradle.kts | 1 + 20 files changed, 570 insertions(+), 1 deletion(-) create mode 100644 core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiModule.kt create mode 100644 core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapRepositoryModule.kt create mode 100644 core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/DefaultEventMapRepository.kt create mode 100644 core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiClient.kt create mode 100644 core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/FakeEventMapApiClient.kt create mode 100644 core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/response/EventMapResponse.kt create mode 100644 core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapEvent.kt create mode 100644 core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapRepository.kt create mode 100644 feature/eventmap/.gitignore create mode 100644 feature/eventmap/build.gradle.kts create mode 100644 feature/eventmap/proguard-rules.pro create mode 100644 feature/eventmap/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenTest.kt create mode 100644 feature/eventmap/src/androidUnitTest/resources/robolectric.properties create mode 100644 feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt create mode 100644 feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenPresenter.kt create mode 100644 feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/component/EventMapItem.kt create mode 100644 feature/eventmap/src/iosMain/kotlin/io/github/droidkaigi/confsched/eventmap/IosCompose.kt diff --git a/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiModule.kt b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiModule.kt new file mode 100644 index 000000000..77e6213f2 --- /dev/null +++ b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiModule.kt @@ -0,0 +1,15 @@ +package io.github.droidkaigi.confsched.data.eventmap + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +public class EventMapApiModule { + @Provides + public fun provideEventMapApi(): EventMapApiClient { + return FakeEventMapApiClient() + } +} diff --git a/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapRepositoryModule.kt b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapRepositoryModule.kt new file mode 100644 index 000000000..97ffaa3a1 --- /dev/null +++ b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapRepositoryModule.kt @@ -0,0 +1,34 @@ +package io.github.droidkaigi.confsched.data.eventmap + +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import io.github.droidkaigi.confsched.data.di.RepositoryQualifier +import io.github.droidkaigi.confsched.model.EventMapRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +public abstract class EventMapRepositoryModule { + @Binds + @RepositoryQualifier + @IntoMap + @ClassKey(EventMapRepository::class) + public abstract fun bind(repository: EventMapRepository): Any + + public companion object { + @Provides + @Singleton + public fun provideEventMapRepository( + eventMapApi: EventMapApiClient, + ): EventMapRepository { + return DefaultEventMapRepository( + eventMapApi = eventMapApi, + ) + } + } +} diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/DefaultEventMapRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/DefaultEventMapRepository.kt new file mode 100644 index 000000000..f83847fe5 --- /dev/null +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/DefaultEventMapRepository.kt @@ -0,0 +1,36 @@ +package io.github.droidkaigi.confsched.data.eventmap + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import io.github.droidkaigi.confsched.compose.SafeLaunchedEffect +import io.github.droidkaigi.confsched.compose.safeCollectAsState +import io.github.droidkaigi.confsched.model.EventMapEvent +import io.github.droidkaigi.confsched.model.EventMapRepository +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow + +public class DefaultEventMapRepository( + private val eventMapApi: EventMapApiClient, +) : EventMapRepository { + private val eventMapStateFlow = + MutableStateFlow>(persistentListOf()) + + @Composable + override fun eventMapEvents(): PersistentList { + val eventMap by eventMapStateFlow.safeCollectAsState() + SafeLaunchedEffect(Unit) { + if (eventMap.isEmpty()) { + refresh() + } + } + return eventMap + } + + override suspend fun refresh() { + eventMapApi + .eventMapEvents() + .toPersistentList().also { eventMapStateFlow.value = it } + } +} diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiClient.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiClient.kt new file mode 100644 index 000000000..dee8bc5e2 --- /dev/null +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/EventMapApiClient.kt @@ -0,0 +1,21 @@ +package io.github.droidkaigi.confsched.data.eventmap + +import de.jensklingenberg.ktorfit.http.GET +import io.github.droidkaigi.confsched.data.eventmap.response.EventMapResponse +import io.github.droidkaigi.confsched.model.EventMapEvent +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal interface EventMapApi { + @GET("/events/droidkaigi2023/eventmap") + suspend fun getEventMap(): EventMapResponse +} + +public interface EventMapApiClient { + + public suspend fun eventMapEvents(): PersistentList +} + +public fun EventMapResponse.toEventMapList(): PersistentList { + return listOf(EventMapEvent()).toPersistentList() +} diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/FakeEventMapApiClient.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/FakeEventMapApiClient.kt new file mode 100644 index 000000000..baa0036da --- /dev/null +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/FakeEventMapApiClient.kt @@ -0,0 +1,33 @@ +package io.github.droidkaigi.confsched.data.eventmap + +import io.github.droidkaigi.confsched.data.eventmap.response.EventMapResponse +import io.github.droidkaigi.confsched.model.EventMapEvent +import kotlinx.collections.immutable.PersistentList +import okio.IOException + +public class FakeEventMapApiClient : EventMapApiClient { + + public sealed class Status : EventMapApiClient { + public data object Operational : Status() { + override suspend fun eventMapEvents(): PersistentList { + return EventMapResponse().toEventMapList() + } + } + + public data object Error : Status() { + override suspend fun eventMapEvents(): PersistentList { + throw IOException("Fake IO Exception") + } + } + } + + private var status: Status = Status.Operational + + public fun setup(status: Status) { + this.status = status + } + + override suspend fun eventMapEvents(): PersistentList { + return status.eventMapEvents() + } +} diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/response/EventMapResponse.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/response/EventMapResponse.kt new file mode 100644 index 000000000..b8047c5c7 --- /dev/null +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/eventmap/response/EventMapResponse.kt @@ -0,0 +1,3 @@ +package io.github.droidkaigi.confsched.data.eventmap.response + +public class EventMapResponse diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapEvent.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapEvent.kt new file mode 100644 index 000000000..82dcba0e7 --- /dev/null +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapEvent.kt @@ -0,0 +1,3 @@ +package io.github.droidkaigi.confsched.model + +class EventMapEvent diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapRepository.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapRepository.kt new file mode 100644 index 000000000..f5f988d35 --- /dev/null +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/EventMapRepository.kt @@ -0,0 +1,18 @@ +package io.github.droidkaigi.confsched.model + +import androidx.compose.runtime.Composable +import io.github.droidkaigi.confsched.model.compositionlocal.LocalRepositories +import kotlinx.collections.immutable.PersistentList + +interface EventMapRepository { + + suspend fun refresh() + + @Composable + fun eventMapEvents(): PersistentList +} + +@Composable +fun localEventMapRepository(): EventMapRepository { + return LocalRepositories.current[EventMapRepository::class] as EventMapRepository +} diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt index 059b985fd..f1919e237 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt @@ -3,6 +3,8 @@ package io.github.droidkaigi.confsched.testing import androidx.compose.ui.test.junit4.ComposeTestRule import io.github.droidkaigi.confsched.data.contributors.ContributorsApiClient import io.github.droidkaigi.confsched.data.contributors.FakeContributorsApiClient +import io.github.droidkaigi.confsched.data.eventmap.EventMapApiClient +import io.github.droidkaigi.confsched.data.eventmap.FakeEventMapApiClient import io.github.droidkaigi.confsched.data.sessions.FakeSessionsApiClient import io.github.droidkaigi.confsched.data.sessions.SessionsApiClient import io.github.droidkaigi.confsched.testing.coroutines.runTestWithLogging @@ -136,3 +138,25 @@ class DefaultContributorsServerRobot @Inject constructor(contributorsApiClient: ) } } + +interface EventMapServerRobot { + enum class ServerStatus { + Operational, + Error, + } + + fun setupContributorServer(serverStatus: ServerStatus) +} + +class DefaultEventMapServerRobot @Inject constructor(contributorsApiClient: EventMapApiClient) : + EventMapServerRobot { + private val fakeEventMapApiClient = contributorsApiClient as FakeEventMapApiClient + override fun setupContributorServer(serverStatus: EventMapServerRobot.ServerStatus) { + fakeEventMapApiClient.setup( + when (serverStatus) { + EventMapServerRobot.ServerStatus.Operational -> FakeEventMapApiClient.Status.Operational + EventMapServerRobot.ServerStatus.Error -> FakeEventMapApiClient.Status.Error + }, + ) + } +} diff --git a/feature/eventmap/.gitignore b/feature/eventmap/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/eventmap/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/eventmap/build.gradle.kts b/feature/eventmap/build.gradle.kts new file mode 100644 index 000000000..42f680e8a --- /dev/null +++ b/feature/eventmap/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("droidkaigi.convention.kmpfeature") +} + +android.namespace = "io.github.droidkaigi.confsched.feature.eventMap" +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(projects.core.model) + implementation(projects.core.ui) + implementation(libs.kotlinxCoroutinesCore) + implementation(projects.core.designsystem) + implementation(libs.moleculeRuntime) + } + } + androidUnitTest { + dependencies { + implementation(projects.core.testing) + } + } + } +} diff --git a/feature/eventmap/proguard-rules.pro b/feature/eventmap/proguard-rules.pro new file mode 100644 index 000000000..ff59496d8 --- /dev/null +++ b/feature/eventmap/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/eventmap/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenTest.kt b/feature/eventmap/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenTest.kt new file mode 100644 index 000000000..b2c0c06cf --- /dev/null +++ b/feature/eventmap/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenTest.kt @@ -0,0 +1,66 @@ +package io.github.droidkaigi.confsched.eventmap + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.droidkaigi.confsched.testing.DefaultEventMapServerRobot +import io.github.droidkaigi.confsched.testing.DefaultScreenRobot +import io.github.droidkaigi.confsched.testing.EventMapServerRobot +import io.github.droidkaigi.confsched.testing.RobotTestRule +import io.github.droidkaigi.confsched.testing.ScreenRobot +import io.github.droidkaigi.confsched.testing.runRobot +import io.github.droidkaigi.confsched.testing.todoChecks +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import javax.inject.Inject + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +class EventMapScreenTest { + + @get:Rule + @BindValue val robotTestRule: RobotTestRule = RobotTestRule(this) + + @Inject + lateinit var eventMapScreenRobot: EventMapScreenRobot + + @Test + fun checkScreenContent() { + runRobot(eventMapScreenRobot) { + setupContributorServer(EventMapServerRobot.ServerStatus.Operational) + setupScreenContent() + + captureScreenWithChecks( + checks = todoChecks("This screen is still empty now. Please add some checks."), + ) + } + } + + @Test + fun checkErrorScreenContent() { + runRobot(eventMapScreenRobot) { + setupContributorServer(EventMapServerRobot.ServerStatus.Error) + setupScreenContent() + + captureScreenWithChecks( + checks = todoChecks("This screen is still empty now. Please add some checks."), + ) + } + } +} + +class EventMapScreenRobot @Inject constructor( + screenRobot: DefaultScreenRobot, + eventMapServerRobot: DefaultEventMapServerRobot, +) : ScreenRobot by screenRobot, + EventMapServerRobot by eventMapServerRobot { + fun setupScreenContent() { + robotTestRule.setContent { + EventMapScreen( + onNavigationIconClick = { }, + onEventMapItemClick = { }, + ) + } + } +} diff --git a/feature/eventmap/src/androidUnitTest/resources/robolectric.properties b/feature/eventmap/src/androidUnitTest/resources/robolectric.properties new file mode 100644 index 000000000..273245ecb --- /dev/null +++ b/feature/eventmap/src/androidUnitTest/resources/robolectric.properties @@ -0,0 +1,7 @@ +sdk=34 +# RobolectricDeviceQualifiers.NexusOne +qualifiers=w320dp-h533dp-normal-long-notround-any-hdpi-keyshidden-trackball + +application=dagger.hilt.android.testing.HiltTestApplication +# https://github.com/robolectric/robolectric/issues/6593 +instrumentedPackages=androidx.loader.content diff --git a/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt new file mode 100644 index 000000000..c0b4c6f5f --- /dev/null +++ b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt @@ -0,0 +1,165 @@ +package io.github.droidkaigi.confsched.eventmap + +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.material.icons.Icons.AutoMirrored.Filled +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.testTag +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import co.touchlab.kermit.Logger +import io.github.droidkaigi.confsched.compose.rememberEventEmitter +import io.github.droidkaigi.confsched.eventmap.component.EventMapItem +import io.github.droidkaigi.confsched.model.EventMapEvent +import io.github.droidkaigi.confsched.ui.SnackbarMessageEffect +import io.github.droidkaigi.confsched.ui.UserMessageStateHolder +import io.github.droidkaigi.confsched.ui.handleOnClickIfNotNavigating +import kotlinx.collections.immutable.PersistentList + +const val eventMapScreenRoute = "eventMap" +const val EventMapScreenTestTag = "EventMapScreenTestTag" + +fun NavGraphBuilder.eventMapScreens( + onNavigationIconClick: () -> Unit, + onContributorItemClick: (url: String) -> Unit, +) { + composable(eventMapScreenRoute) { + val lifecycleOwner = LocalLifecycleOwner.current + EventMapScreen( + onNavigationIconClick = { + handleOnClickIfNotNavigating( + lifecycleOwner, + onNavigationIconClick, + ) + }, + onEventMapItemClick = onContributorItemClick, + ) + } +} + +data class EventMapUiState( + val eventMap: PersistentList, + val userMessageStateHolder: UserMessageStateHolder, +) + +@Composable +fun EventMapScreen( + onNavigationIconClick: () -> Unit, + onEventMapItemClick: (url: String) -> Unit, + modifier: Modifier = Modifier, + isTopAppBarHidden: Boolean = false, +) { + val eventEmitter = rememberEventEmitter() + val uiState = eventMapScreenPresenter( + events = eventEmitter, + ) + + val snackbarHostState = remember { SnackbarHostState() } + + SnackbarMessageEffect( + snackbarHostState = snackbarHostState, + userMessageStateHolder = uiState.userMessageStateHolder, + ) + EventMapScreen( + uiState = uiState, + isTopAppBarHidden = isTopAppBarHidden, + snackbarHostState = snackbarHostState, + onBackClick = onNavigationIconClick, + onEventMapItemClick = onEventMapItemClick, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EventMapScreen( + uiState: EventMapUiState, + snackbarHostState: SnackbarHostState, + onBackClick: () -> Unit, + onEventMapItemClick: (url: String) -> Unit, + isTopAppBarHidden: Boolean, + modifier: Modifier = Modifier, +) { + Logger.d { "EventMapScreen: $uiState" } + val scrollBehavior = + if (!isTopAppBarHidden) { + TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + } else { + null + } + Scaffold( + modifier = modifier.testTag(EventMapScreenTestTag), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + if (scrollBehavior != null) { + LargeTopAppBar( + title = { + Text(text = "EventMapEvent") + }, + navigationIcon = { + IconButton( + onClick = onBackClick, + ) { + Icon( + imageVector = Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + scrollBehavior = scrollBehavior, + ) + } + }, + ) { padding -> + EventMap( + eventMapEvents = uiState.eventMap, + onEventMapItemClick = onEventMapItemClick, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .let { + if (scrollBehavior != null) { + it.nestedScroll(scrollBehavior.nestedScrollConnection) + } else { + it + } + }, + ) + } +} + +@Composable +private fun EventMap( + eventMapEvents: PersistentList, + onEventMapItemClick: (url: String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + ) { + items(eventMapEvents) { + EventMapItem( + eventMapEvent = it, + onClick = onEventMapItemClick, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} diff --git a/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenPresenter.kt b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenPresenter.kt new file mode 100644 index 000000000..1e39a79aa --- /dev/null +++ b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreenPresenter.kt @@ -0,0 +1,28 @@ +package io.github.droidkaigi.confsched.eventmap + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import io.github.droidkaigi.confsched.compose.SafeLaunchedEffect +import io.github.droidkaigi.confsched.model.EventMapRepository +import io.github.droidkaigi.confsched.model.localEventMapRepository +import io.github.droidkaigi.confsched.ui.providePresenterDefaults +import kotlinx.coroutines.flow.Flow + +sealed interface EventMapScreenEvent + +@Composable +fun eventMapScreenPresenter( + events: Flow, + eventMapRepository: EventMapRepository = localEventMapRepository(), +): EventMapUiState = providePresenterDefaults { userMessageStateHolder -> + val eventMap by rememberUpdatedState(eventMapRepository.eventMapEvents()) + SafeLaunchedEffect(Unit) { + events.collect { + } + } + EventMapUiState( + eventMap = eventMap, + userMessageStateHolder = userMessageStateHolder, + ) +} diff --git a/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/component/EventMapItem.kt b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/component/EventMapItem.kt new file mode 100644 index 000000000..f5d5cb167 --- /dev/null +++ b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/component/EventMapItem.kt @@ -0,0 +1,39 @@ +package io.github.droidkaigi.confsched.eventmap.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +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.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.github.droidkaigi.confsched.model.EventMapEvent + +@Composable +fun EventMapItem( + eventMapEvent: EventMapEvent, + @Suppress("UnusedParameter") + onClick: (url: String) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .clickable { +// eventMapEvent.profileUrl?.let(onClick) + } + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(23.dp), + ) { + Text( + text = "EventMapItem + $eventMapEvent", + style = MaterialTheme.typography.bodyLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } +} diff --git a/feature/eventmap/src/iosMain/kotlin/io/github/droidkaigi/confsched/eventmap/IosCompose.kt b/feature/eventmap/src/iosMain/kotlin/io/github/droidkaigi/confsched/eventmap/IosCompose.kt new file mode 100644 index 000000000..bada267b3 --- /dev/null +++ b/feature/eventmap/src/iosMain/kotlin/io/github/droidkaigi/confsched/eventmap/IosCompose.kt @@ -0,0 +1,31 @@ +package io.github.droidkaigi.confsched.eventmap + +import io.github.droidkaigi.confsched.data.Repositories +import io.github.droidkaigi.confsched.ui.composeViewController +import io.github.droidkaigi.confsched.ui.presenterStateFlow +import kotlinx.coroutines.flow.Flow +import platform.UIKit.UIViewController +import kotlin.reflect.KClass + +@Suppress("UNUSED") +fun eventMapViewController( + repositories: Repositories, + onEventMapItemClick: (url: String) -> Unit, +): UIViewController = composeViewController(repositories) { + EventMapScreen( + isTopAppBarHidden = true, + onNavigationIconClick = { /* no action for iOS side */ }, + onEventMapItemClick = onEventMapItemClick, + ) +} + +@Suppress("unused") +fun eventMapScreenPresenterStateFlow( + repositories: Map, Any>, + events: Flow, +): Flow = presenterStateFlow( + events = events, + repositories = repositories, +) { + eventMapScreenPresenter(events) +} diff --git a/gradle.properties b/gradle.properties index c0adf3db9..6ced4ac6b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ app.ios.shared.debug=true # gradle -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx6048m -Dfile.encoding=UTF-8 org.gradle.configureondemand=true org.gradle.caching=true org.gradle.parallel=true diff --git a/settings.gradle.kts b/settings.gradle.kts index ad3779140..b8c46244b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ include( ":feature:main", ":feature:sessions", ":feature:contributors", + ":feature:eventmap", ":core:designsystem", ":core:data", ":core:model", From 6b04a1153f98dda69f0a9ca0928eb2d2998f7a7e Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 18 Jun 2024 10:31:37 +0900 Subject: [PATCH 2/2] Add event map to tabs --- app-android/build.gradle.kts | 1 + .../io/github/droidkaigi/confsched/KaigiApp.kt | 14 ++++++++++---- app-ios-shared/build.gradle.kts | 1 + .../confsched/shared/IosComposeKaigiApp.kt | 9 +++++---- .../confsched/testing/robot/KaigiAppRobot.kt | 4 ++-- .../confsched/eventmap/EventMapScreen.kt | 16 ++++++++++++++-- .../droidkaigi/confsched/main/MainScreen.kt | 8 ++++---- .../confsched/main/strings/MainStrings.kt | 6 +++--- 8 files changed, 40 insertions(+), 19 deletions(-) diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts index 287a18875..f19bf69ea 100644 --- a/app-android/build.gradle.kts +++ b/app-android/build.gradle.kts @@ -99,6 +99,7 @@ dependencies { implementation(projects.feature.main) implementation(projects.feature.contributors) implementation(projects.feature.sessions) + implementation(projects.feature.eventmap) implementation(projects.core.model) implementation(projects.core.data) implementation(projects.core.designsystem) diff --git a/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt b/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt index 0e8004370..58a4efabe 100644 --- a/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt +++ b/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt @@ -29,11 +29,13 @@ import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import io.github.droidkaigi.confsched.contributors.contributorsScreenRoute import io.github.droidkaigi.confsched.contributors.contributorsScreens import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched.eventmap.eventMapScreens +import io.github.droidkaigi.confsched.eventmap.navigateEventMapScreen import io.github.droidkaigi.confsched.main.MainNestedGraphStateHolder import io.github.droidkaigi.confsched.main.MainScreenTab import io.github.droidkaigi.confsched.main.MainScreenTab.About -import io.github.droidkaigi.confsched.main.MainScreenTab.Achievements -import io.github.droidkaigi.confsched.main.MainScreenTab.FloorMap +import io.github.droidkaigi.confsched.main.MainScreenTab.EventMap +import io.github.droidkaigi.confsched.main.MainScreenTab.ProfileCard import io.github.droidkaigi.confsched.main.MainScreenTab.Timetable import io.github.droidkaigi.confsched.main.mainScreen import io.github.droidkaigi.confsched.main.mainScreenRoute @@ -109,6 +111,10 @@ private fun NavGraphBuilder.mainScreen( onTimetableItemClick = navController::navigateToTimetableItemDetailScreen, contentPadding = contentPadding, ) + eventMapScreens( + onNavigationIconClick = navController::popBackStack, + onEventMapItemClick = externalNavController::navigate, + ) }, ) } @@ -129,9 +135,9 @@ class KaigiAppMainNestedGraphStateHolder : MainNestedGraphStateHolder { ) { when (tab) { Timetable -> mainNestedNavController.navigateTimetableScreen() + EventMap -> mainNestedNavController.navigateEventMapScreen() About -> TODO() - FloorMap -> TODO() - Achievements -> TODO() + ProfileCard -> TODO() } } } diff --git a/app-ios-shared/build.gradle.kts b/app-ios-shared/build.gradle.kts index f75da8a62..f23a4a5ea 100644 --- a/app-ios-shared/build.gradle.kts +++ b/app-ios-shared/build.gradle.kts @@ -53,6 +53,7 @@ kotlin { api(projects.core.ui) api(projects.feature.main) api(projects.feature.sessions) + api(projects.feature.eventmap) api(projects.feature.contributors) implementation(libs.kotlinxCoroutinesCore) implementation(libs.skieAnnotation) diff --git a/app-ios-shared/src/commonMain/kotlin/io/github/droidkaigi/confsched/shared/IosComposeKaigiApp.kt b/app-ios-shared/src/commonMain/kotlin/io/github/droidkaigi/confsched/shared/IosComposeKaigiApp.kt index 6810506d6..9b61da96e 100644 --- a/app-ios-shared/src/commonMain/kotlin/io/github/droidkaigi/confsched/shared/IosComposeKaigiApp.kt +++ b/app-ios-shared/src/commonMain/kotlin/io/github/droidkaigi/confsched/shared/IosComposeKaigiApp.kt @@ -20,11 +20,12 @@ import io.github.droidkaigi.confsched.contributors.ContributorsScreen import io.github.droidkaigi.confsched.contributors.contributorsScreenRoute import io.github.droidkaigi.confsched.data.Repositories import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched.eventmap.navigateEventMapScreen import io.github.droidkaigi.confsched.main.MainNestedGraphStateHolder import io.github.droidkaigi.confsched.main.MainScreenTab import io.github.droidkaigi.confsched.main.MainScreenTab.About -import io.github.droidkaigi.confsched.main.MainScreenTab.Achievements -import io.github.droidkaigi.confsched.main.MainScreenTab.FloorMap +import io.github.droidkaigi.confsched.main.MainScreenTab.EventMap +import io.github.droidkaigi.confsched.main.MainScreenTab.ProfileCard import io.github.droidkaigi.confsched.main.MainScreenTab.Timetable import io.github.droidkaigi.confsched.main.mainScreen import io.github.droidkaigi.confsched.main.mainScreenRoute @@ -138,9 +139,9 @@ class KaigiAppMainNestedGraphStateHolder : MainNestedGraphStateHolder { ) { when (tab) { Timetable -> mainNestedNavController.navigateTimetableScreen() + EventMap -> mainNestedNavController.navigateEventMapScreen() About -> TODO() - FloorMap -> TODO() - Achievements -> TODO() + ProfileCard -> TODO() } } } diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/KaigiAppRobot.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/KaigiAppRobot.kt index 28fac9b44..73caa7eab 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/KaigiAppRobot.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/KaigiAppRobot.kt @@ -22,7 +22,7 @@ class KaigiAppRobot @Inject constructor( fun goToFloorMap() { composeTestRule - .onNode(hasTestTag(MainScreenTab.FloorMap.testTag)) + .onNode(hasTestTag(MainScreenTab.About.testTag)) .performClick() waitUntilIdle() } @@ -30,7 +30,7 @@ class KaigiAppRobot @Inject constructor( fun goToAchievements() { composeTestRule .onAllNodes( - matcher = hasTestTag(MainScreenTab.Achievements.testTag), + matcher = hasTestTag(MainScreenTab.ProfileCard.testTag), useUnmergedTree = true, ) .onFirst() diff --git a/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt index c0b4c6f5f..b77cda763 100644 --- a/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt +++ b/feature/eventmap/src/commonMain/kotlin/io/github/droidkaigi/confsched/eventmap/EventMapScreen.kt @@ -22,6 +22,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import co.touchlab.kermit.Logger @@ -38,7 +40,7 @@ const val EventMapScreenTestTag = "EventMapScreenTestTag" fun NavGraphBuilder.eventMapScreens( onNavigationIconClick: () -> Unit, - onContributorItemClick: (url: String) -> Unit, + onEventMapItemClick: (url: String) -> Unit, ) { composable(eventMapScreenRoute) { val lifecycleOwner = LocalLifecycleOwner.current @@ -49,11 +51,21 @@ fun NavGraphBuilder.eventMapScreens( onNavigationIconClick, ) }, - onEventMapItemClick = onContributorItemClick, + onEventMapItemClick = onEventMapItemClick, ) } } +fun NavController.navigateEventMapScreen() { + navigate(eventMapScreenRoute) { + popUpTo(route = checkNotNull(graph.findStartDestination().route)) { + saveState = true + } + launchSingleTop = true + restoreState = true + } +} + data class EventMapUiState( val eventMap: PersistentList, val userMessageStateHolder: UserMessageStateHolder, diff --git a/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/MainScreen.kt b/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/MainScreen.kt index 05d597106..a84b9e860 100644 --- a/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/MainScreen.kt +++ b/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/MainScreen.kt @@ -130,15 +130,15 @@ enum class MainScreenTab( ), @OptIn(ExperimentalResourceApi::class) - FloorMap( + EventMap( icon = IconRepresentation.Vector(Icons.Outlined.Map), selectedIcon = IconRepresentation.Drawable(drawableId = Res.drawable.icon_map_fill), - label = MainStrings.FloorMap.asString(), - contentDescription = MainStrings.FloorMap.asString(), + label = MainStrings.EventMap.asString(), + contentDescription = MainStrings.EventMap.asString(), ), @OptIn(ExperimentalResourceApi::class) - Achievements( + ProfileCard( icon = IconRepresentation.Drawable(drawableId = Res.drawable.icon_achievement_outline), selectedIcon = IconRepresentation.Drawable(drawableId = Res.drawable.icon_achievement_fill), label = MainStrings.Achievements.asString(), diff --git a/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/strings/MainStrings.kt b/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/strings/MainStrings.kt index 5103b6f15..eadd34847 100644 --- a/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/strings/MainStrings.kt +++ b/feature/main/src/commonMain/kotlin/io/github/droidkaigi/confsched/main/strings/MainStrings.kt @@ -6,7 +6,7 @@ import io.github.droidkaigi.confsched.designsystem.strings.StringsBindings sealed class MainStrings : Strings(Bindings) { data object Timetable : MainStrings() - data object FloorMap : MainStrings() + data object EventMap : MainStrings() data object Achievements : MainStrings() data object About : MainStrings() data object Contributors : MainStrings() @@ -16,7 +16,7 @@ sealed class MainStrings : Strings(Bindings) { Lang.Japanese to { item, _ -> when (item) { Timetable -> "Timetable" - FloorMap -> "Floor Map" + EventMap -> "Event Map" Achievements -> "Achievements" About -> "About" Contributors -> "Contributors" @@ -26,7 +26,7 @@ sealed class MainStrings : Strings(Bindings) { Lang.English to { item, bindings -> when (item) { Timetable -> "Timetable" - FloorMap -> "Floor Map" + EventMap -> "Event Map" Achievements -> "Achievements" About -> "About" Contributors -> "Contributors"