From 3a3147a706d4e63bb501c91f397ab93a8d0a42ed Mon Sep 17 00:00:00 2001 From: hiroaki404 Date: Wed, 4 Sep 2024 19:00:14 +0900 Subject: [PATCH 1/9] implements auto scroll to current screen --- .../confsched/model/TimetableItem.kt | 11 +++++--- .../sessions/SearchScreenPresenter.kt | 4 +-- .../sessions/TimetableScreenPresenter.kt | 4 +-- .../confsched/sessions/section/SearchList.kt | 1 + .../sessions/section/TimetableGrid.kt | 28 +++++++++++++++++++ .../sessions/section/TimetableList.kt | 26 +++++++++++++---- 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt index 5790ba60f..0bcdb5cb9 100644 --- a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt @@ -71,13 +71,11 @@ public sealed class TimetableItem { } public val startsTimeString: String by lazy { - val localDate = startsAt.toLocalDateTime(TimeZone.currentSystemDefault()) - "${localDate.hour}".padStart(2, '0') + ":" + "${localDate.minute}".padStart(2, '0') + startsAt.toTimetableTimeString() } public val endsTimeString: String by lazy { - val localDate = endsAt.toLocalDateTime(TimeZone.currentSystemDefault()) - "${localDate.hour}".padStart(2, '0') + ":" + "${localDate.minute}".padStart(2, '0') + endsAt.toTimetableTimeString() } private val minutesString: String by lazy { @@ -121,6 +119,11 @@ public sealed class TimetableItem { } } +public fun Instant.toTimetableTimeString(): String { + val localDate = toLocalDateTime(TimeZone.currentSystemDefault()) + return "${localDate.hour}".padStart(2, '0') + ":" + "${localDate.minute}".padStart(2, '0') +} + public fun Session.Companion.fake(): Session { return Session( id = TimetableItemId("2"), diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt index f5c778dcc..546fe08f3 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt @@ -174,8 +174,8 @@ fun searchScreenPresenter( timetableListUiState = TimetableListUiState( timetableItemMap = filteredSessions.groupBy { TimetableListUiState.TimeSlot( - startTimeString = it.startsTimeString, - endTimeString = it.endsTimeString, + startTime = it.startsAt, + endTime = it.endsAt, ) }.mapValues { entries -> entries.value.sortedWith( diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt index 401ade996..4c8067377 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt @@ -101,8 +101,8 @@ fun timetableSheet( ), ).timetableItems.groupBy { TimetableListUiState.TimeSlot( - startTimeString = it.startsTimeString, - endTimeString = it.endsTimeString, + startTime = it.startsAt, + endTime = it.endsAt, ) }.mapValues { entries -> entries.value.sortedWith( diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/SearchList.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/SearchList.kt index 3ba8bf250..40e947945 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/SearchList.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/SearchList.kt @@ -24,6 +24,7 @@ fun SearchList( contentPadding = contentPadding, highlightWord = highlightWord, modifier = modifier, + enableAutoScrolling = false, timetableItemTagsContent = { timetableItem -> timetableItem.day?.monthAndDay()?.let { monthAndDay -> TimetableItemTag(tagText = monthAndDay) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt index c0f74e210..8d5b6d8d3 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf @@ -91,7 +92,9 @@ import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone +import kotlinx.datetime.atTime import kotlinx.datetime.minus +import kotlinx.datetime.periodUntil import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.ui.tooling.preview.Preview @@ -218,6 +221,7 @@ fun TimetableGrid( ) { val coroutineScope = rememberCoroutineScope() val density = timetableState.density + val clock = LocalClock.current val verticalScale = timetableState.screenScaleState.verticalScale val timetableLayout = remember(timetable, verticalScale) { TimetableLayout(timetable = timetable, density = density, verticalScale = verticalScale) @@ -248,6 +252,30 @@ fun TimetableGrid( val currentTimeLineColor = MaterialTheme.colorScheme.primary val currentTimeDotRadius = with(timetableState.density) { TimetableSizes.currentTimeDotRadius.toPx() } + LaunchedEffect(Unit) { + val progressingSession = + timetable.timetableItems.timetableItems.find { clock.now() in it.startsAt..it.endsAt } + progressingSession?.let { session -> + val timeZone = TimeZone.of("UTC+9") + val period = with(session.startsAt) { + toLocalDateTime(timeZone) + .date.atTime(10, 0) + .toInstant(timeZone) + .periodUntil(this, timeZone) + } + val minuteHeightPx = + with(density) { TimetableSizes.minuteHeight.times(verticalScale).toPx() } + val scrollOffsetY = + -with(period) { hours * minuteHeightPx * 60 + minutes * minuteHeightPx } + timetableScreen.scroll( + Offset(0f, scrollOffsetY), + 0, + Offset.Zero, + nestedScrollDispatcher, + ) + } + } + LazyLayout( modifier = modifier .focusGroup() diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt index 522f4f005..64aff7a31 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -39,15 +40,18 @@ import io.github.droidkaigi.confsched.droidkaigiui.component.TimetableItemCard import io.github.droidkaigi.confsched.droidkaigiui.component.TimetableItemTag import io.github.droidkaigi.confsched.droidkaigiui.component.TimetableTime import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalAnimatedVisibilityScope +import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalClock import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalSharedTransitionScope import io.github.droidkaigi.confsched.droidkaigiui.icon import io.github.droidkaigi.confsched.model.Timetable import io.github.droidkaigi.confsched.model.TimetableItem +import io.github.droidkaigi.confsched.model.toTimetableTimeString import io.github.droidkaigi.confsched.sessions.component.TimetableNestedScrollStateHolder import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNestedScrollConnection import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNestedScrollStateHolder import io.github.droidkaigi.confsched.sessions.timetableDetailSharedContentStateKey import kotlinx.collections.immutable.PersistentMap +import kotlinx.datetime.Instant const val TimetableListTestTag = "TimetableList" @@ -56,10 +60,10 @@ data class TimetableListUiState( val timetable: Timetable, ) { data class TimeSlot( - val startTimeString: String, - val endTimeString: String, + val startTime: Instant, + val endTime: Instant, ) { - val key: String get() = "$startTimeString-$endTimeString" + val key: String get() = "$startTime-$endTime" } } @@ -75,8 +79,10 @@ internal fun TimetableList( modifier: Modifier = Modifier, nestedScrollStateHolder: TimetableNestedScrollStateHolder = rememberTimetableNestedScrollStateHolder(true), highlightWord: String = "", + enableAutoScrolling: Boolean = true, ) { val layoutDirection = LocalLayoutDirection.current + val clock = LocalClock.current val sharedTransitionScope = LocalSharedTransitionScope.current val animatedScope = LocalAnimatedVisibilityScope.current val windowSize = calculateWindowSizeClass() @@ -92,6 +98,16 @@ internal fun TimetableList( nestedScrollStateHolder = nestedScrollStateHolder, ) + LaunchedEffect(Unit) { + if (enableAutoScrolling) { + val progressingSessionIndex = + uiState.timetableItemMap.keys.indexOfFirst { clock.now() in it.startTime..it.endTime } + progressingSessionIndex.takeIf { it != -1 }?.let { + scrollState.animateScrollToItem(it) + } + } + } + LazyColumn( modifier = modifier.testTag(TimetableListTestTag) .offset { @@ -136,8 +152,8 @@ internal fun TimetableList( timeTextHeight = it.size.height } .offset { IntOffset(0, timeTextOffset) }, - startTime = time.startTimeString, - endTime = time.endTimeString, + startTime = time.startTime.toTimetableTimeString(), + endTime = time.endTime.toTimetableTimeString(), ) Spacer(modifier = Modifier.width(12.dp)) Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { From 3c5571d5c05663d5fa6f9d047622a6f7e9519f69 Mon Sep 17 00:00:00 2001 From: hiroaki404 Date: Thu, 5 Sep 2024 07:18:24 +0900 Subject: [PATCH 2/9] consider break time --- .../confsched/sessions/section/TimetableGrid.kt | 7 +++++-- .../confsched/sessions/section/TimetableList.kt | 12 +++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt index 8d5b6d8d3..629b850c2 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt @@ -253,8 +253,11 @@ fun TimetableGrid( val currentTimeDotRadius = with(timetableState.density) { TimetableSizes.currentTimeDotRadius.toPx() } LaunchedEffect(Unit) { - val progressingSession = - timetable.timetableItems.timetableItems.find { clock.now() in it.startsAt..it.endsAt } + val progressingSession = timetable.timetableItems.timetableItems + .windowed(2, 1, true) + .find { clock.now() in it.first().startsAt..it.last().startsAt } + ?.first() + progressingSession?.let { session -> val timeZone = TimeZone.of("UTC+9") val period = with(session.startsAt) { diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt index 64aff7a31..47c510887 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt @@ -77,7 +77,9 @@ internal fun TimetableList( contentPadding: PaddingValues, timetableItemTagsContent: @Composable RowScope.(TimetableItem) -> Unit, modifier: Modifier = Modifier, - nestedScrollStateHolder: TimetableNestedScrollStateHolder = rememberTimetableNestedScrollStateHolder(true), + nestedScrollStateHolder: TimetableNestedScrollStateHolder = rememberTimetableNestedScrollStateHolder( + true + ), highlightWord: String = "", enableAutoScrolling: Boolean = true, ) { @@ -100,8 +102,12 @@ internal fun TimetableList( LaunchedEffect(Unit) { if (enableAutoScrolling) { - val progressingSessionIndex = - uiState.timetableItemMap.keys.indexOfFirst { clock.now() in it.startTime..it.endTime } + val progressingSessionIndex = uiState.timetableItemMap.keys + .windowed(2, 1, true) + .indexOfFirst { + clock.now() in it.first().startTime..it.last().startTime + } + progressingSessionIndex.takeIf { it != -1 }?.let { scrollState.animateScrollToItem(it) } From 6668aa3c76af1368fb8da70a6073ad952fc3080e Mon Sep 17 00:00:00 2001 From: hiroaki404 Date: Thu, 5 Sep 2024 08:22:24 +0900 Subject: [PATCH 3/9] scroll to last session --- .../sessions/section/TimetableGrid.kt | 18 ++++++++++++- .../sessions/section/TimetableList.kt | 25 ++++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt index 629b850c2..f7a7eff07 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt @@ -75,6 +75,7 @@ import io.github.droidkaigi.confsched.model.DroidKaigi2024Day import io.github.droidkaigi.confsched.model.TimeLine import io.github.droidkaigi.confsched.model.Timetable import io.github.droidkaigi.confsched.model.TimetableItem +import io.github.droidkaigi.confsched.model.TimetableItem.Session import io.github.droidkaigi.confsched.model.TimetableRoom import io.github.droidkaigi.confsched.model.TimetableRooms import io.github.droidkaigi.confsched.model.fake @@ -92,9 +93,11 @@ import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.atTime import kotlinx.datetime.minus import kotlinx.datetime.periodUntil +import kotlinx.datetime.plus import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.ui.tooling.preview.Preview @@ -254,12 +257,25 @@ fun TimetableGrid( LaunchedEffect(Unit) { val progressingSession = timetable.timetableItems.timetableItems + .run { + // Insert dummy at a position after the start of the last session to allow scrolling + val endOfTheDayInstant = first().startsAt.toLocalDateTime(TimeZone.currentSystemDefault()) + .date + .plus(1, DateTimeUnit.DAY) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + plus( + Session.Companion.fake().copy( + startsAt = endOfTheDayInstant, + endsAt = endOfTheDayInstant, + ), + ) + } .windowed(2, 1, true) .find { clock.now() in it.first().startsAt..it.last().startsAt } ?.first() progressingSession?.let { session -> - val timeZone = TimeZone.of("UTC+9") + val timeZone = TimeZone.currentSystemDefault() val period = with(session.startsAt) { toLocalDateTime(timeZone) .date.atTime(10, 0) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt index 47c510887..1a27338db 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt @@ -49,9 +49,15 @@ import io.github.droidkaigi.confsched.model.toTimetableTimeString import io.github.droidkaigi.confsched.sessions.component.TimetableNestedScrollStateHolder import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNestedScrollConnection import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNestedScrollStateHolder +import io.github.droidkaigi.confsched.sessions.section.TimetableListUiState.TimeSlot import io.github.droidkaigi.confsched.sessions.timetableDetailSharedContentStateKey import kotlinx.collections.immutable.PersistentMap +import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime const val TimetableListTestTag = "TimetableList" @@ -78,7 +84,7 @@ internal fun TimetableList( timetableItemTagsContent: @Composable RowScope.(TimetableItem) -> Unit, modifier: Modifier = Modifier, nestedScrollStateHolder: TimetableNestedScrollStateHolder = rememberTimetableNestedScrollStateHolder( - true + true, ), highlightWord: String = "", enableAutoScrolling: Boolean = true, @@ -100,16 +106,29 @@ internal fun TimetableList( nestedScrollStateHolder = nestedScrollStateHolder, ) - LaunchedEffect(Unit) { + LaunchedEffect(uiState) { if (enableAutoScrolling) { val progressingSessionIndex = uiState.timetableItemMap.keys + .run { + // Insert dummy at a position after the start of the last session to allow scrolling + val endOfTheDayInstant = first().startTime.toLocalDateTime(TimeZone.currentSystemDefault()) + .date + .plus(1, DateTimeUnit.DAY) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + plus( + TimeSlot( + startTime = endOfTheDayInstant, + endTime = endOfTheDayInstant, + ), + ) + } .windowed(2, 1, true) .indexOfFirst { clock.now() in it.first().startTime..it.last().startTime } progressingSessionIndex.takeIf { it != -1 }?.let { - scrollState.animateScrollToItem(it) + scrollState.scrollToItem(it) } } } From 8651bac155c1b75024fb00a80fb53fe2e021d93f Mon Sep 17 00:00:00 2001 From: hiroaki404 Date: Thu, 5 Sep 2024 08:37:35 +0900 Subject: [PATCH 4/9] scroll only when open screen --- .../droidkaigi/confsched/sessions/section/TimetableList.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt index 1a27338db..2abafc6dc 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt @@ -106,7 +106,7 @@ internal fun TimetableList( nestedScrollStateHolder = nestedScrollStateHolder, ) - LaunchedEffect(uiState) { + LaunchedEffect(Unit) { if (enableAutoScrolling) { val progressingSessionIndex = uiState.timetableItemMap.keys .run { From 5d5b3668faef50a1c29d3b3885e333d6bda9501c Mon Sep 17 00:00:00 2001 From: hiroaki404 Date: Thu, 5 Sep 2024 08:59:48 +0900 Subject: [PATCH 5/9] add ui test --- .../confsched/sessions/TimetableScreenTest.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt index bcc315cec..f813ac717 100644 --- a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt +++ b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt @@ -180,6 +180,46 @@ class TimetableScreenTest(private val testCase: DescribedBehavior + val formattedDateTime = + case.dateTime.format(LocalDateTime.Format { byUnicodePattern("yyyy-MM-dd HH-mm") }) + describe("when the current datetime is $formattedDateTime") { + doIt { + setupTimetableServer(ServerStatus.Operational) + setupTimetableScreenContent(case.dateTime) + } + + val formattedTime = + case.dateTime.time.format(LocalTime.Format { byUnicodePattern("HH-mm") }) + val description = "show an timetable item of the current time at $formattedTime" + itShould(description) { + captureScreenWithChecks { + checkTimetableListDisplayed() + } + } + } + } listOf( TimeLineTestSpec( dateTime = LocalDateTime( From 859acc06261fbdd05e685f60f4f3fd9679f99822 Mon Sep 17 00:00:00 2001 From: hiroaki404 Date: Thu, 5 Sep 2024 18:11:41 +0900 Subject: [PATCH 6/9] rename and add function --- .../sessions/section/TimetableGrid.kt | 80 +++++++++++-------- .../sessions/section/TimetableList.kt | 41 +++++----- .../sessions/section/TimetableSheet.kt | 30 ++++++- 3 files changed, 96 insertions(+), 55 deletions(-) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt index f7a7eff07..ac846aa8a 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableGrid.kt @@ -86,6 +86,8 @@ import io.github.droidkaigi.confsched.sessions.component.TimetableGridItem import io.github.droidkaigi.confsched.sessions.component.TimetableGridRooms import io.github.droidkaigi.confsched.sessions.section.ScreenScrollState.Companion import io.github.droidkaigi.confsched.sessions.timetableDetailSharedContentStateKey +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.datetime.DateTimeUnit @@ -116,6 +118,7 @@ fun TimetableGrid( onTimetableItemClick: (TimetableItem) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), + scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(), ) { TimetableGrid( timetable = uiState.timetable, @@ -125,6 +128,7 @@ fun TimetableGrid( onTimetableItemClick = onTimetableItemClick, modifier = modifier, contentPadding = contentPadding, + scrolledToCurrentTimeState = scrolledToCurrentTimeState, ) } @@ -138,6 +142,7 @@ fun TimetableGrid( onTimetableItemClick: (TimetableItem) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), + scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(), ) { val coroutineScope = rememberCoroutineScope() val layoutDirection = LocalLayoutDirection.current @@ -184,6 +189,7 @@ fun TimetableGrid( start = 16.dp + contentPadding.calculateStartPadding(layoutDirection), end = 16.dp + contentPadding.calculateEndPadding(layoutDirection), ), + scrolledToCurrentTimeState = scrolledToCurrentTimeState, ) { timetableItem, itemHeightPx -> val timetableGridItemModifier = if (sharedTransitionScope != null && animatedScope != null) { with(sharedTransitionScope) { @@ -220,6 +226,7 @@ fun TimetableGrid( selectedDay: DroidKaigi2024Day, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), + scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(), content: @Composable (TimetableItem, Int) -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -256,42 +263,33 @@ fun TimetableGrid( val currentTimeDotRadius = with(timetableState.density) { TimetableSizes.currentTimeDotRadius.toPx() } LaunchedEffect(Unit) { - val progressingSession = timetable.timetableItems.timetableItems - .run { - // Insert dummy at a position after the start of the last session to allow scrolling - val endOfTheDayInstant = first().startsAt.toLocalDateTime(TimeZone.currentSystemDefault()) - .date - .plus(1, DateTimeUnit.DAY) - .atStartOfDayIn(TimeZone.currentSystemDefault()) - plus( - Session.Companion.fake().copy( - startsAt = endOfTheDayInstant, - endsAt = endOfTheDayInstant, - ), + if (scrolledToCurrentTimeState.inTimetableGrid.not()) { + val progressingSession = timetable.timetableItems.timetableItems + .insertDummyEndOfTheDayItem() // Insert dummy at a position after last session to allow scrolling + .windowed(2, 1, true) + .find { clock.now() in it.first().startsAt..it.last().startsAt } + ?.firstOrNull() + + progressingSession?.let { session -> + val timeZone = TimeZone.currentSystemDefault() + val period = with(session.startsAt) { + toLocalDateTime(timeZone) + .date.atTime(10, 0) + .toInstant(timeZone) + .periodUntil(this, timeZone) + } + val minuteHeightPx = + with(density) { TimetableSizes.minuteHeight.times(verticalScale).toPx() } + val scrollOffsetY = + -with(period) { hours * minuteHeightPx * 60 + minutes * minuteHeightPx } + timetableScreen.scroll( + Offset(0f, scrollOffsetY), + 0, + Offset.Zero, + nestedScrollDispatcher, ) + scrolledToCurrentTimeState.scrolledInTimetableGrid() } - .windowed(2, 1, true) - .find { clock.now() in it.first().startsAt..it.last().startsAt } - ?.first() - - progressingSession?.let { session -> - val timeZone = TimeZone.currentSystemDefault() - val period = with(session.startsAt) { - toLocalDateTime(timeZone) - .date.atTime(10, 0) - .toInstant(timeZone) - .periodUntil(this, timeZone) - } - val minuteHeightPx = - with(density) { TimetableSizes.minuteHeight.times(verticalScale).toPx() } - val scrollOffsetY = - -with(period) { hours * minuteHeightPx * 60 + minutes * minuteHeightPx } - timetableScreen.scroll( - Offset(0f, scrollOffsetY), - 0, - Offset.Zero, - nestedScrollDispatcher, - ) } } @@ -1029,3 +1027,17 @@ object TimetableSizes { val minuteHeight = 4.dp val currentTimeDotRadius = 6.dp } + +private fun PersistentList.insertDummyEndOfTheDayItem(): PersistentList { + val endOfTheDayInstant = + first().startsAt.toLocalDateTime(TimeZone.currentSystemDefault()) + .date + .plus(1, DateTimeUnit.DAY) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + return plus( + Session.Companion.fake().copy( + startsAt = endOfTheDayInstant, + endsAt = endOfTheDayInstant, + ), + ).toPersistentList() +} diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt index 2abafc6dc..abbfacade 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt @@ -51,7 +51,9 @@ import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNested import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNestedScrollStateHolder import io.github.droidkaigi.confsched.sessions.section.TimetableListUiState.TimeSlot import io.github.droidkaigi.confsched.sessions.timetableDetailSharedContentStateKey +import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.toImmutableSet import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone @@ -83,11 +85,10 @@ internal fun TimetableList( contentPadding: PaddingValues, timetableItemTagsContent: @Composable RowScope.(TimetableItem) -> Unit, modifier: Modifier = Modifier, - nestedScrollStateHolder: TimetableNestedScrollStateHolder = rememberTimetableNestedScrollStateHolder( - true, - ), + nestedScrollStateHolder: TimetableNestedScrollStateHolder = rememberTimetableNestedScrollStateHolder(true), highlightWord: String = "", enableAutoScrolling: Boolean = true, + scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(), ) { val layoutDirection = LocalLayoutDirection.current val clock = LocalClock.current @@ -107,29 +108,16 @@ internal fun TimetableList( ) LaunchedEffect(Unit) { - if (enableAutoScrolling) { + if (enableAutoScrolling && scrolledToCurrentTimeState.inTimetableList.not()) { val progressingSessionIndex = uiState.timetableItemMap.keys - .run { - // Insert dummy at a position after the start of the last session to allow scrolling - val endOfTheDayInstant = first().startTime.toLocalDateTime(TimeZone.currentSystemDefault()) - .date - .plus(1, DateTimeUnit.DAY) - .atStartOfDayIn(TimeZone.currentSystemDefault()) - plus( - TimeSlot( - startTime = endOfTheDayInstant, - endTime = endOfTheDayInstant, - ), - ) - } + .insertDummyEndOfTheDayItem() // Insert dummy at a position after last session to allow scrolling .windowed(2, 1, true) - .indexOfFirst { - clock.now() in it.first().startTime..it.last().startTime - } + .indexOfFirst { clock.now() in it.first().startTime..it.last().startTime } progressingSessionIndex.takeIf { it != -1 }?.let { scrollState.scrollToItem(it) } + scrolledToCurrentTimeState.scrolledInTimetableList() } } @@ -240,3 +228,16 @@ internal fun TimetableList( } } } + +private fun ImmutableSet.insertDummyEndOfTheDayItem(): ImmutableSet { + val endOfTheDayInstant = first().startTime.toLocalDateTime(TimeZone.currentSystemDefault()) + .date + .plus(1, DateTimeUnit.DAY) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + return plus( + TimeSlot( + startTime = endOfTheDayInstant, + endTime = endOfTheDayInstant, + ), + ).toImmutableSet() +} diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt index 2216d1837..cb0bbdff9 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -67,7 +68,14 @@ fun Timetable( } val layoutDirection = LocalLayoutDirection.current - val nestedScrollStateHolder = rememberTimetableNestedScrollStateHolder(isListTimetable = uiState is ListTimetable) + val nestedScrollStateHolder = + rememberTimetableNestedScrollStateHolder(isListTimetable = uiState is ListTimetable) + val scrolledToCurrentTimeState = rememberSaveable( + saver = listSaver( + save = { listOf(it.inTimetableList, it.inTimetableGrid) }, + restore = { ScrolledToCurrentTimeState(it[0], it[1]) }, + ), + ) { ScrolledToCurrentTimeState() } Surface( modifier = modifier.padding(contentPadding.calculateTopPadding()), @@ -109,6 +117,7 @@ fun Timetable( start = contentPadding.calculateStartPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection), ), + scrolledToCurrentTimeState = scrolledToCurrentTimeState, timetableItemTagsContent = { timetableItem -> timetableItem.language.labels.forEach { label -> TimetableItemTag(tagText = label) @@ -132,6 +141,7 @@ fun Timetable( bottom = contentPadding.calculateBottomPadding(), start = contentPadding.calculateStartPadding(layoutDirection), ), + scrolledToCurrentTimeState = scrolledToCurrentTimeState, ) } @@ -166,3 +176,21 @@ private fun rememberGridTimetableStates(): Map Date: Fri, 6 Sep 2024 08:00:54 +0900 Subject: [PATCH 7/9] use LocalTime for groupBy --- .../confsched/model/TimetableItem.kt | 16 +++++++++- .../sessions/SearchScreenPresenter.kt | 4 +-- .../sessions/TimetableScreenPresenter.kt | 4 +-- .../sessions/section/TimetableList.kt | 32 +++++++++++-------- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt index 0bcdb5cb9..53f0740c6 100644 --- a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/TimetableItem.kt @@ -7,6 +7,7 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime @@ -78,6 +79,14 @@ public sealed class TimetableItem { endsAt.toTimetableTimeString() } + public val startsLocalTime: LocalTime by lazy { + startsAt.toLocalTime() + } + + public val endsLocalTime: LocalTime by lazy { + endsAt.toLocalTime() + } + private val minutesString: String by lazy { val minutes = (endsAt - startsAt) .toComponents { minutes, _, _ -> minutes } @@ -119,11 +128,16 @@ public sealed class TimetableItem { } } -public fun Instant.toTimetableTimeString(): String { +private fun Instant.toTimetableTimeString(): String { val localDate = toLocalDateTime(TimeZone.currentSystemDefault()) return "${localDate.hour}".padStart(2, '0') + ":" + "${localDate.minute}".padStart(2, '0') } +private fun Instant.toLocalTime(): LocalTime { + val localDateTime = toLocalDateTime(TimeZone.currentSystemDefault()) + return localDateTime.time +} + public fun Session.Companion.fake(): Session { return Session( id = TimetableItemId("2"), diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt index 546fe08f3..f5613eb06 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/SearchScreenPresenter.kt @@ -174,8 +174,8 @@ fun searchScreenPresenter( timetableListUiState = TimetableListUiState( timetableItemMap = filteredSessions.groupBy { TimetableListUiState.TimeSlot( - startTime = it.startsAt, - endTime = it.endsAt, + startTime = it.startsLocalTime, + endTime = it.endsLocalTime, ) }.mapValues { entries -> entries.value.sortedWith( diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt index 4c8067377..574eb4970 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenPresenter.kt @@ -101,8 +101,8 @@ fun timetableSheet( ), ).timetableItems.groupBy { TimetableListUiState.TimeSlot( - startTime = it.startsAt, - endTime = it.endsAt, + startTime = it.startsLocalTime, + endTime = it.endsLocalTime, ) }.mapValues { entries -> entries.value.sortedWith( diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt index abbfacade..82f486158 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableList.kt @@ -45,7 +45,6 @@ import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalSharedT import io.github.droidkaigi.confsched.droidkaigiui.icon import io.github.droidkaigi.confsched.model.Timetable import io.github.droidkaigi.confsched.model.TimetableItem -import io.github.droidkaigi.confsched.model.toTimetableTimeString import io.github.droidkaigi.confsched.sessions.component.TimetableNestedScrollStateHolder import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNestedScrollConnection import io.github.droidkaigi.confsched.sessions.component.rememberTimetableNestedScrollStateHolder @@ -54,11 +53,9 @@ import io.github.droidkaigi.confsched.sessions.timetableDetailSharedContentState import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.toImmutableSet -import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.Instant +import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn -import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime const val TimetableListTestTag = "TimetableList" @@ -68,10 +65,17 @@ data class TimetableListUiState( val timetable: Timetable, ) { data class TimeSlot( - val startTime: Instant, - val endTime: Instant, + val startTime: LocalTime, + val endTime: LocalTime, ) { + val startTimeString: String get() = startTime.toTimetableTimeString() + val endTimeString: String get() = endTime.toTimetableTimeString() + val key: String get() = "$startTime-$endTime" + + private fun LocalTime.toTimetableTimeString(): String { + return "$hour".padStart(2, '0') + ":" + "$minute".padStart(2, '0') + } } } @@ -112,7 +116,7 @@ internal fun TimetableList( val progressingSessionIndex = uiState.timetableItemMap.keys .insertDummyEndOfTheDayItem() // Insert dummy at a position after last session to allow scrolling .windowed(2, 1, true) - .indexOfFirst { clock.now() in it.first().startTime..it.last().startTime } + .indexOfFirst { clock.now().toLocalTime() in it.first().startTime..it.last().startTime } progressingSessionIndex.takeIf { it != -1 }?.let { scrollState.scrollToItem(it) @@ -165,8 +169,8 @@ internal fun TimetableList( timeTextHeight = it.size.height } .offset { IntOffset(0, timeTextOffset) }, - startTime = time.startTime.toTimetableTimeString(), - endTime = time.endTime.toTimetableTimeString(), + startTime = time.startTimeString, + endTime = time.endTimeString, ) Spacer(modifier = Modifier.width(12.dp)) Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { @@ -230,10 +234,7 @@ internal fun TimetableList( } private fun ImmutableSet.insertDummyEndOfTheDayItem(): ImmutableSet { - val endOfTheDayInstant = first().startTime.toLocalDateTime(TimeZone.currentSystemDefault()) - .date - .plus(1, DateTimeUnit.DAY) - .atStartOfDayIn(TimeZone.currentSystemDefault()) + val endOfTheDayInstant = LocalTime(23, 59, 59) return plus( TimeSlot( startTime = endOfTheDayInstant, @@ -241,3 +242,8 @@ private fun ImmutableSet.insertDummyEndOfTheDayItem(): ImmutableSet Date: Sun, 8 Sep 2024 17:57:07 +0900 Subject: [PATCH 8/9] If it is not the day of the conference, do not scroll --- .../droidkaigi/confsched/sessions/section/TimetableSheet.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt index cb0bbdff9..f5f608f6c 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/section/TimetableSheet.kt @@ -118,6 +118,7 @@ fun Timetable( end = contentPadding.calculateEndPadding(layoutDirection), ), scrolledToCurrentTimeState = scrolledToCurrentTimeState, + enableAutoScrolling = clock.now() in selectedDay.start..selectedDay.end, timetableItemTagsContent = { timetableItem -> timetableItem.language.labels.forEach { label -> TimetableItemTag(tagText = label) From 7734bbb3259057b1eff0a627ff96aeacffe015b8 Mon Sep 17 00:00:00 2001 From: hiroaki404 Date: Tue, 10 Sep 2024 09:34:33 +0900 Subject: [PATCH 9/9] update ui test --- .../confsched/sessions/TimetableScreenTest.kt | 71 +++++++------------ .../sessions/section/TimetableList.kt | 2 +- 2 files changed, 26 insertions(+), 47 deletions(-) diff --git a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt index f813ac717..8dbe39273 100644 --- a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt +++ b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt @@ -185,9 +185,9 @@ class TimetableScreenTest(private val testCase: DescribedBehavior - val formattedDateTime = - case.dateTime.format(LocalDateTime.Format { byUnicodePattern("yyyy-MM-dd HH-mm") }) - describe("when the current datetime is $formattedDateTime") { - doIt { - setupTimetableServer(ServerStatus.Operational) - setupTimetableScreenContent(case.dateTime) - } - - val formattedTime = - case.dateTime.time.format(LocalTime.Format { byUnicodePattern("HH-mm") }) - val description = "show an timetable item of the current time at $formattedTime" - itShould(description) { - captureScreenWithChecks { - checkTimetableListDisplayed() - } - } - } - } - listOf( TimeLineTestSpec( dateTime = LocalDateTime( year = 2024, monthNumber = 9, - dayOfMonth = 11, - hour = 10, + dayOfMonth = 12, + hour = 23, minute = 0, ), shouldShowTimeLine = false, ), - TimeLineTestSpec( - dateTime = LocalDateTime( - year = 2024, - monthNumber = 9, - dayOfMonth = 12, - hour = 10, - minute = 30, - ), - shouldShowTimeLine = true, - ), TimeLineTestSpec( dateTime = LocalDateTime( year = 2024, @@ -258,19 +228,28 @@ class TimetableScreenTest(private val testCase: DescribedBehavior