Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GT-1991 Switch to an event object for callbacks on the Dashboard #3011

Merged
merged 6 commits into from
Aug 1, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ class DashboardActivity : BaseActivity() {
setContent {
GodToolsTheme {
DashboardLayout(
onOpenTool = { t, lang1, lang2 -> openTool(t, *listOfNotNull(lang1, lang2).toTypedArray()) },
onOpenToolDetails = { startToolDetailsActivity(it) }
onEvent = {
when (it) {
is DashboardEvent.OpenTool ->
openTool(it.tool, *listOfNotNull(it.lang1, it.lang2).toTypedArray())
is DashboardEvent.OpenToolDetails -> {
it.tool?.code?.let { startToolDetailsActivity(it) }
}
}
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import kotlinx.coroutines.launch
import org.ccci.gto.android.common.androidx.compose.material3.ui.navigationdrawer.toggle
import org.ccci.gto.android.common.androidx.compose.material3.ui.pullrefresh.PullRefreshIndicator
import org.ccci.gto.android.common.androidx.lifecycle.compose.OnResume
import org.cru.godtools.BuildConfig
import org.cru.godtools.R
import org.cru.godtools.analytics.compose.RecordAnalyticsScreen
import org.cru.godtools.analytics.firebase.model.ACTION_IAM_ALL_TOOLS
Expand All @@ -50,17 +51,25 @@ import org.cru.godtools.base.ui.theme.GodToolsTheme
import org.cru.godtools.model.Tool
import org.cru.godtools.shared.analytics.AnalyticsScreenNames
import org.cru.godtools.ui.dashboard.home.AllFavoritesList
import org.cru.godtools.ui.dashboard.home.DashboardHomeEvent
import org.cru.godtools.ui.dashboard.home.HomeContent
import org.cru.godtools.ui.dashboard.lessons.DashboardLessonsEvent
import org.cru.godtools.ui.dashboard.lessons.LessonsLayout
import org.cru.godtools.ui.dashboard.tools.ToolsLayout
import org.cru.godtools.ui.drawer.DrawerMenuLayout
import org.cru.godtools.ui.tools.ToolCardEvent

internal sealed interface DashboardEvent {
open class OpenTool(val tool: Tool?, val lang1: Locale?, val lang2: Locale?) : DashboardEvent
class OpenLesson(tool: Tool?, lang: Locale?) : OpenTool(tool, lang, null)
class OpenToolDetails(val tool: Tool?) : DashboardEvent
}

@Composable
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
internal fun DashboardLayout(
onEvent: (DashboardEvent) -> Unit,
viewModel: DashboardViewModel = viewModel(),
onOpenTool: (Tool?, Locale?, Locale?) -> Unit,
onOpenToolDetails: (String) -> Unit
) {
val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(DrawerValue.Closed)
Expand Down Expand Up @@ -104,32 +113,52 @@ internal fun DashboardLayout(
saveableStateHolder.SaveableStateProvider(page) {
when (page) {
Page.LESSONS -> LessonsLayout(
onOpenLesson = { tool, translation ->
onOpenTool(
tool,
translation?.languageCode,
null
)
}
onEvent = {
when (it) {
is DashboardLessonsEvent.OpenLesson ->
onEvent(DashboardEvent.OpenLesson(it.tool, it.lang))
}
},
)

Page.HOME -> HomeContent(
onOpenTool = onOpenTool,
onOpenToolDetails = onOpenToolDetails,
onViewAllFavorites = {
saveableStateHolder.removeState(Page.FAVORITE_TOOLS)
viewModel.updateCurrentPage(Page.FAVORITE_TOOLS, false)
},
onViewAllTools = { viewModel.updateCurrentPage(Page.ALL_TOOLS) },
onEvent = {
when (it) {
DashboardHomeEvent.ViewAllFavorites -> {
saveableStateHolder.removeState(Page.FAVORITE_TOOLS)
viewModel.updateCurrentPage(Page.FAVORITE_TOOLS, false)
}
DashboardHomeEvent.ViewAllTools -> viewModel.updateCurrentPage(Page.ALL_TOOLS)
is DashboardHomeEvent.OpenTool ->
onEvent(DashboardEvent.OpenTool(it.tool, it.lang1, it.lang2))
is DashboardHomeEvent.OpenToolDetails ->
onEvent(DashboardEvent.OpenToolDetails(it.tool))
}
}
)

Page.FAVORITE_TOOLS -> AllFavoritesList(
onOpenTool = onOpenTool,
onOpenToolDetails = onOpenToolDetails
onEvent = {
when (it) {
is ToolCardEvent.Click,
is ToolCardEvent.OpenTool ->
onEvent(DashboardEvent.OpenTool(it.tool, it.lang1, it.lang2))
is ToolCardEvent.OpenToolDetails ->
onEvent(DashboardEvent.OpenToolDetails(it.tool))
}
},
)

Page.ALL_TOOLS -> ToolsLayout(
onToolClicked = onOpenToolDetails
onEvent = {
when (it) {
is ToolCardEvent.Click,
is ToolCardEvent.OpenToolDetails ->
onEvent(DashboardEvent.OpenToolDetails(it.tool))
is ToolCardEvent.OpenTool ->
if (BuildConfig.DEBUG) error("opening a tool from All Tools is unsupported")
}
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.cru.godtools.ui.dashboard.home

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
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.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable
import org.cru.godtools.R
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL_DETAILS
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_FAVORITE
import org.cru.godtools.ui.tools.PreloadTool
import org.cru.godtools.ui.tools.ToolCard
import org.cru.godtools.ui.tools.ToolCardEvent

@Composable
@OptIn(ExperimentalFoundationApi::class)
internal fun AllFavoritesList(
onEvent: (ToolCardEvent) -> Unit,
viewModel: HomeViewModel = viewModel(),
) {
val favoriteTools by viewModel.reorderableFavoriteTools.collectAsState()

val reorderableState = rememberReorderableLazyListState(
// only support reordering tool items
canDragOver = { (_, k), _ -> (k as? String)?.startsWith("tool:") == true },
onMove = { from, to ->
val fromPos = favoriteTools.indexOfFirst { it.code == (from.key as? String)?.removePrefix("tool:") }
val toPos = favoriteTools.indexOfFirst { it.code == (to.key as? String)?.removePrefix("tool:") }
if (fromPos != -1 && toPos != -1) viewModel.moveFavoriteTool(fromPos, toPos)
},
onDragEnd = { _, _ -> viewModel.commitFavoriteToolOrder() }
)

LazyColumn(
contentPadding = PaddingValues(16.dp),
state = reorderableState.listState,
modifier = Modifier
.reorderable(reorderableState)
.detectReorderAfterLongPress(reorderableState)
) {
item("header", "header") {
Text(
stringResource(R.string.dashboard_home_section_favorites_title),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.fillMaxWidth()
.animateItemPlacement()
)
}

items(favoriteTools, key = { "tool:${it.code}" }) { tool ->
ReorderableItem(reorderableState, "tool:${tool.code}") { isDragging ->
val interactionSource = remember { MutableInteractionSource() }
interactionSource.reorderableDragInteractions(isDragging)

PreloadTool(tool)
ToolCard(
toolCode = tool.code.orEmpty(),
confirmRemovalFromFavorites = true,
interactionSource = interactionSource,
onEvent = {
when (it) {
is ToolCardEvent.Click,
is ToolCardEvent.OpenTool -> viewModel.recordOpenClickInAnalytics(
ACTION_OPEN_TOOL,
it.tool?.code,
SOURCE_FAVORITE
)
is ToolCardEvent.OpenToolDetails -> viewModel.recordOpenClickInAnalytics(
ACTION_OPEN_TOOL_DETAILS,
it.tool?.code,
SOURCE_FAVORITE
)
}
onEvent(it)
},
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
)
}
}
}
}

@Composable
private fun MutableInteractionSource.reorderableDragInteractions(isDragging: Boolean) {
val dragState = remember { object { var start: DragInteraction.Start? = null } }
LaunchedEffect(isDragging) {
when (val start = dragState.start) {
null -> if (isDragging) dragState.start = DragInteraction.Start().also { emit(it) }
else -> if (!isDragging) {
dragState.start = null
emit(DragInteraction.Stop(start))
}
}
}
}
Loading