Skip to content

Commit

Permalink
Merge pull request #972 from hiroaki404/feature/auto_scroll_to_curren…
Browse files Browse the repository at this point in the history
…t_sesion

Feature/auto scroll to current sesion
  • Loading branch information
takahirom authored Sep 12, 2024
2 parents fc8059e + 7734bbb commit cf4f87e
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -71,13 +72,19 @@ 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()
}

public val startsLocalTime: LocalTime by lazy {
startsAt.toLocalTime()
}

public val endsLocalTime: LocalTime by lazy {
endsAt.toLocalTime()
}

private val minutesString: String by lazy {
Expand Down Expand Up @@ -121,6 +128,16 @@ public sealed class TimetableItem {
}
}

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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ class TimetableScreenTest(private val testCase: DescribedBehavior<TimetableScree
),
shouldShowTimeLine = true,
),
TimeLineTestSpec(
dateTime = LocalDateTime(
year = 2024,
monthNumber = 9,
dayOfMonth = 12,
hour = 23,
minute = 0,
),
shouldShowTimeLine = false,
),
TimeLineTestSpec(
dateTime = LocalDateTime(
year = 2024,
Expand All @@ -218,19 +228,28 @@ class TimetableScreenTest(private val testCase: DescribedBehavior<TimetableScree
doIt {
setupTimetableServer(ServerStatus.Operational)
setupTimetableScreenContent(case.dateTime)
clickTimetableUiTypeChangeButton()
}

val formattedTime =
case.dateTime.time.format(LocalTime.Format { byUnicodePattern("HH-mm") })
val description = if (case.shouldShowTimeLine) {
"show an indicator of the current time at $formattedTime"
} else {
"not show an indicator of the current time"
}
itShould(description) {
val timetableListDescription = "show an timetable item of the current time at $formattedTime"
itShould(timetableListDescription) {
captureScreenWithChecks {
checkTimetableGridDisplayed()
checkTimetableListDisplayed()
}
}
describe("switch to grid timetable") {
doIt {
clickTimetableUiTypeChangeButton()
}
val timetableGridDescription = if (case.shouldShowTimeLine) {
"show an indicator of the current time at $formattedTime"
} else {
"not show an indicator of the current time"
}
itShould(timetableGridDescription) {
captureScreenWithChecks {
checkTimetableGridDisplayed()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ fun searchScreenPresenter(
timetableListUiState = TimetableListUiState(
timetableItemMap = filteredSessions.groupBy {
TimetableListUiState.TimeSlot(
startTimeString = it.startsTimeString,
endTimeString = it.endsTimeString,
startTime = it.startsLocalTime,
endTime = it.endsLocalTime,
)
}.mapValues { entries ->
entries.value.sortedWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ fun timetableSheet(
),
).timetableItems.groupBy {
TimetableListUiState.TimeSlot(
startTimeString = it.startsTimeString,
endTimeString = it.endsTimeString,
startTime = it.startsLocalTime,
endTime = it.endsLocalTime,
)
}.mapValues { entries ->
entries.value.sortedWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fun SearchList(
contentPadding = contentPadding,
highlightWord = highlightWord,
modifier = modifier,
enableAutoScrolling = false,
timetableItemTagsContent = { timetableItem ->
timetableItem.day?.monthAndDay()?.let { monthAndDay ->
TimetableItemTag(tagText = monthAndDay)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,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
Expand All @@ -84,14 +86,20 @@ 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
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
Expand All @@ -110,6 +118,7 @@ fun TimetableGrid(
onTimetableItemClick: (TimetableItem) -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(),
) {
TimetableGrid(
timetable = uiState.timetable,
Expand All @@ -119,6 +128,7 @@ fun TimetableGrid(
onTimetableItemClick = onTimetableItemClick,
modifier = modifier,
contentPadding = contentPadding,
scrolledToCurrentTimeState = scrolledToCurrentTimeState,
)
}

Expand All @@ -132,6 +142,7 @@ fun TimetableGrid(
onTimetableItemClick: (TimetableItem) -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(),
) {
val coroutineScope = rememberCoroutineScope()
val layoutDirection = LocalLayoutDirection.current
Expand Down Expand Up @@ -178,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) {
Expand Down Expand Up @@ -214,10 +226,12 @@ fun TimetableGrid(
selectedDay: DroidKaigi2024Day,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(),
content: @Composable (TimetableItem, Int) -> Unit,
) {
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)
Expand Down Expand Up @@ -248,6 +262,37 @@ fun TimetableGrid(
val currentTimeLineColor = MaterialTheme.colorScheme.primary
val currentTimeDotRadius = with(timetableState.density) { TimetableSizes.currentTimeDotRadius.toPx() }

LaunchedEffect(Unit) {
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()
}
}
}

LazyLayout(
modifier = modifier
.focusGroup()
Expand Down Expand Up @@ -982,3 +1027,17 @@ object TimetableSizes {
val minuteHeight = 4.dp
val currentTimeDotRadius = 6.dp
}

private fun PersistentList<TimetableItem>.insertDummyEndOfTheDayItem(): PersistentList<TimetableItem> {
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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,15 +40,23 @@ 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.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.ImmutableSet
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime

const val TimetableListTestTag = "TimetableList"

Expand All @@ -56,10 +65,17 @@ data class TimetableListUiState(
val timetable: Timetable,
) {
data class TimeSlot(
val startTimeString: String,
val endTimeString: String,
val startTime: LocalTime,
val endTime: LocalTime,
) {
val key: String get() = "$startTimeString-$endTimeString"
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')
}
}
}

Expand All @@ -75,8 +91,11 @@ internal fun TimetableList(
modifier: Modifier = Modifier,
nestedScrollStateHolder: TimetableNestedScrollStateHolder = rememberTimetableNestedScrollStateHolder(true),
highlightWord: String = "",
enableAutoScrolling: Boolean = true,
scrolledToCurrentTimeState: ScrolledToCurrentTimeState = ScrolledToCurrentTimeState(),
) {
val layoutDirection = LocalLayoutDirection.current
val clock = LocalClock.current
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedScope = LocalAnimatedVisibilityScope.current
val windowSize = calculateWindowSizeClass()
Expand All @@ -92,6 +111,20 @@ internal fun TimetableList(
nestedScrollStateHolder = nestedScrollStateHolder,
)

LaunchedEffect(Unit) {
if (enableAutoScrolling && scrolledToCurrentTimeState.inTimetableList.not()) {
val progressingSessionIndex = uiState.timetableItemMap.keys
.insertDummyEndOfTheDayItem() // Insert dummy at a position after last session to allow scrolling
.windowed(2, 1, true)
.indexOfFirst { clock.now().toLocalTime() in it.first().startTime..<it.last().startTime }

progressingSessionIndex.takeIf { it != -1 }?.let {
scrollState.scrollToItem(it)
}
scrolledToCurrentTimeState.scrolledInTimetableList()
}
}

LazyColumn(
modifier = modifier.testTag(TimetableListTestTag)
.offset {
Expand Down Expand Up @@ -199,3 +232,18 @@ internal fun TimetableList(
}
}
}

private fun ImmutableSet<TimeSlot>.insertDummyEndOfTheDayItem(): ImmutableSet<TimeSlot> {
val endOfTheDayInstant = LocalTime(23, 59, 59)
return plus(
TimeSlot(
startTime = endOfTheDayInstant,
endTime = endOfTheDayInstant,
),
).toImmutableSet()
}

private fun Instant.toLocalTime(): LocalTime {
val localDateTime = toLocalDateTime(TimeZone.currentSystemDefault())
return localDateTime.time
}
Loading

0 comments on commit cf4f87e

Please sign in to comment.