Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ import org.cru.godtools.R
import org.cru.godtools.analytics.model.ExitLinkActionEvent
import org.cru.godtools.base.ui.compose.LocalEventBus
import org.cru.godtools.model.getDescription
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.UiState

internal const val TEST_TAG_LANGUAGES_AVAILABLE = "languages_available"

@Composable
internal fun ToolDetailsAbout(state: ToolDetailsScreen.State, modifier: Modifier = Modifier) {
internal fun ToolDetailsAbout(state: UiState, modifier: Modifier = Modifier) {
val tool by rememberUpdatedState(state.tool)
val translation by rememberUpdatedState(state.translation)

Expand Down Expand Up @@ -123,7 +124,7 @@ private enum class ToolDetailsAboutAccordionSection { OUTLINE, BIBLE_REFERENCES,
@Composable
@VisibleForTesting
internal fun ToolDetailsLanguages(
state: ToolDetailsScreen.State,
state: UiState,
expanded: Boolean,
onToggleLanguages: () -> Unit,
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -188,7 +189,7 @@ private fun ToolDetailsAboutAccordionSection(
}

@Composable
private fun ToolDetailsLinkifiedText(text: String?, state: ToolDetailsScreen.State, modifier: Modifier = Modifier) {
private fun ToolDetailsLinkifiedText(text: String?, state: UiState, modifier: Modifier = Modifier) {
val eventBus = LocalEventBus.current
val uriHandler = LocalUriHandler.current
val tool by rememberUpdatedState(state.tool?.code)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ import org.cru.godtools.base.ui.theme.GodToolsTheme
import org.cru.godtools.base.ui.youtubeplayer.YouTubePlayer
import org.cru.godtools.model.getName
import org.cru.godtools.ui.drawer.DrawerMenuLayout
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.Event
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.Page
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.State
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.UiEvent
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.UiState
import org.cru.godtools.ui.tooldetails.analytics.model.ToolDetailsScreenEvent
import org.cru.godtools.ui.tools.AvailableInLanguage
import org.cru.godtools.ui.tools.DownloadProgressIndicator
Expand All @@ -87,7 +87,7 @@ internal const val TEST_TAG_ACTION_TOOL_TRAINING = "action_tool_training"
@Composable
@CircuitInject(ToolDetailsScreen::class, SingletonComponent::class)
@OptIn(ExperimentalMaterial3Api::class)
fun ToolDetailsLayout(state: State, modifier: Modifier = Modifier) = DrawerMenuLayout(state.drawerState, modifier) {
fun ToolDetailsLayout(state: UiState, modifier: Modifier = Modifier) = DrawerMenuLayout(state.drawerState, modifier) {
val hasShortcut by rememberUpdatedState(state.hasShortcut)
val eventSink by rememberUpdatedState(state.eventSink)

Expand All @@ -97,7 +97,7 @@ fun ToolDetailsLayout(state: State, modifier: Modifier = Modifier) = DrawerMenuL
title = {},
navigationIcon = {
IconButton(
onClick = { eventSink(Event.NavigateUp) },
onClick = { eventSink(UiEvent.NavigateUp) },
modifier = Modifier.testTag(TEST_TAG_ACTION_NAVIGATE_UP),
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
Expand All @@ -116,7 +116,7 @@ fun ToolDetailsLayout(state: State, modifier: Modifier = Modifier) = DrawerMenuL
DropdownMenu(expanded = showOverflow, onDismissRequest = { showOverflow = false }) {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_add_to_home)) },
onClick = { eventSink(Event.PinShortcut) },
onClick = { eventSink(UiEvent.PinShortcut) },
modifier = Modifier.testTag(TEST_TAG_ACTION_PIN_SHORTCUT),
)
}
Expand All @@ -132,7 +132,7 @@ fun ToolDetailsLayout(state: State, modifier: Modifier = Modifier) = DrawerMenuL

@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun ToolDetailsContent(state: State, modifier: Modifier = Modifier) {
private fun ToolDetailsContent(state: UiState, modifier: Modifier = Modifier) {
val tool by rememberUpdatedState(state.tool)
val translation by rememberUpdatedState(state.translation)
val secondLanguage by rememberUpdatedState(state.secondLanguage)
Expand Down Expand Up @@ -231,7 +231,7 @@ private fun ToolDetailsContent(state: State, modifier: Modifier = Modifier) {
}

@Composable
private fun ToolDetailsBanner(state: State, modifier: Modifier = Modifier) {
private fun ToolDetailsBanner(state: UiState, modifier: Modifier = Modifier) {
val youtubeVideo = state.tool?.detailsBannerYoutubeVideoId

when {
Expand Down Expand Up @@ -268,18 +268,18 @@ private fun ToolDetailsBanner(state: State, modifier: Modifier = Modifier) {

@Composable
@VisibleForTesting
internal fun ToolDetailsActions(state: State, modifier: Modifier = Modifier) = Column(modifier = modifier) {
internal fun ToolDetailsActions(state: UiState, modifier: Modifier = Modifier) = Column(modifier = modifier) {
val tool by rememberUpdatedState(state.tool)
val eventSink by rememberUpdatedState(state.eventSink)

Button(
onClick = { eventSink(Event.OpenTool) },
onClick = { eventSink(UiEvent.OpenTool) },
modifier = Modifier.fillMaxWidth()
) { Text(stringResource(R.string.action_tools_open_tool)) }

if (state.hasTips) {
Button(
onClick = { eventSink(Event.OpenToolTraining) },
onClick = { eventSink(UiEvent.OpenToolTraining) },
modifier = Modifier
.testTag(TEST_TAG_ACTION_TOOL_TRAINING)
.fillMaxWidth()
Expand All @@ -288,7 +288,7 @@ internal fun ToolDetailsActions(state: State, modifier: Modifier = Modifier) = C

val isFavorite by remember { derivedStateOf { tool?.isFavorite == true } }
OutlinedButton(
onClick = { eventSink(if (isFavorite) Event.UnpinTool else Event.PinTool) },
onClick = { eventSink(if (isFavorite) UiEvent.UnpinTool else UiEvent.PinTool) },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = if (isFavorite) GodToolsTheme.GT_RED else MaterialTheme.colorScheme.primary
),
Expand All @@ -308,7 +308,7 @@ internal fun ToolDetailsActions(state: State, modifier: Modifier = Modifier) = C
}

@Composable
private fun ToolDetailsVariants(state: State, modifier: Modifier = Modifier) {
private fun ToolDetailsVariants(state: UiState, modifier: Modifier = Modifier) {
val currentTool by rememberUpdatedState(state.toolCode)
val variants by rememberUpdatedState(state.variants)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ import org.cru.godtools.model.Translation
import org.cru.godtools.shortcuts.GodToolsShortcutManager
import org.cru.godtools.sync.GodToolsSyncService
import org.cru.godtools.ui.drawer.DrawerMenuPresenter
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.Event
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.Page
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.State
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.UiEvent
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.UiState
import org.cru.godtools.ui.tools.ToolCard
import org.cru.godtools.ui.tools.ToolCardPresenter
import org.cru.godtools.util.createToolIntent
Expand All @@ -91,9 +91,9 @@ class ToolDetailsPresenter @AssistedInject constructor(
@Named(IS_CONNECTED_STATE_FLOW) private val isConnected: StateFlow<Boolean>,
@Assisted private val screen: ToolDetailsScreen,
@Assisted private val navigator: Navigator,
) : Presenter<State> {
) : Presenter<UiState> {
@Composable
override fun present(): State {
override fun present(): UiState {
val coroutineScope = rememberCoroutineScope()

var toolCode by rememberSaveable { mutableStateOf(screen.initialTool) }
Expand All @@ -114,69 +114,14 @@ class ToolDetailsPresenter @AssistedInject constructor(
val manifest by manifestManager.produceManifestState(translation)
val secondManifest by manifestManager.produceManifestState(secondTranslation)

val eventSink: (Event) -> Unit = remember {
{
when (it) {
Event.NavigateUp -> navigator.pop()

Event.OpenTool -> tool?.let { tool ->
val intent = tool.createToolIntent(
context = context,
languages = when (tool.type) {
Tool.Type.ARTICLE -> listOfNotNull(
secondTranslation?.languageCode ?: translation?.languageCode
)

else -> listOfNotNull(translation?.languageCode, secondTranslation?.languageCode)
},
activeLocale = secondTranslation?.languageCode
)

if (intent != null) {
eventBus.post(OpenAnalyticsActionEvent(ACTION_OPEN_TOOL, tool.code, SOURCE_TOOL_DETAILS))
navigator.goTo(IntentScreen(intent))
}
}

Event.OpenToolTraining -> tool?.let {
// TODO: handle opening training tips and optionally showing the tutorial locally once the
// tutorial uses Circuit.
navigator.goTo(
OpenToolTrainingScreen(
it.code,
it.type,
translation?.languageCode,
secondTranslation?.languageCode
)
)
}

Event.PinTool -> coroutineScope.launch {
settings.setFeatureDiscovered(Settings.FEATURE_TOOL_FAVORITE)
toolsRepository.pinTool(toolCode)
syncService.syncDirtyFavoriteTools()
}

Event.UnpinTool -> coroutineScope.launch {
toolsRepository.unpinTool(toolCode)
syncService.syncDirtyFavoriteTools()
}

is Event.SwitchVariant -> toolCode = it.variant

Event.PinShortcut -> pendingShortcut?.let { shortcutManager.pinShortcut(it) }
}
}
}

val secondLanguage = languagesRepository.rememberLanguage(screen.secondLanguage)
val variants = rememberVariants(tool?.metatoolCode, secondLanguage = secondLanguage, eventSink = eventSink)
val variants = rememberVariants(tool?.metatoolCode, secondLanguage = secondLanguage) { toolCode = it }

// Side Effects
DownloadLatestTranslation(downloadManager, toolCode, translation?.languageCode, isConnected)
DownloadLatestTranslation(downloadManager, toolCode, secondTranslation?.languageCode, isConnected)

return State(
return UiState(
toolCode = toolCode,
tool = tool,
banner = attachmentsRepository.rememberAttachmentFile(
Expand All @@ -194,8 +139,39 @@ class ToolDetailsPresenter @AssistedInject constructor(
availableLanguages = rememberAvailableLanguages(toolCode),
variants = variants,
drawerState = drawerMenuPresenter.present(),
eventSink = eventSink
)
) {
when (it) {
UiEvent.NavigateUp -> navigator.pop()

UiEvent.OpenTool -> openTool(tool, translation, secondTranslation)

UiEvent.OpenToolTraining -> tool?.let {
// TODO: handle opening training tips and optionally showing the tutorial locally once the
// tutorial uses Circuit.
navigator.goTo(
OpenToolTrainingScreen(
it.code,
it.type,
translation?.languageCode,
secondTranslation?.languageCode
)
)
}

UiEvent.PinTool -> coroutineScope.launch {
settings.setFeatureDiscovered(Settings.FEATURE_TOOL_FAVORITE)
toolsRepository.pinTool(toolCode)
syncService.syncDirtyFavoriteTools()
}

UiEvent.UnpinTool -> coroutineScope.launch {
toolsRepository.unpinTool(toolCode)
syncService.syncDirtyFavoriteTools()
}

UiEvent.PinShortcut -> pendingShortcut?.let { shortcutManager.pinShortcut(it) }
}
}
}

@Composable
Expand All @@ -210,11 +186,11 @@ class ToolDetailsPresenter @AssistedInject constructor(
private fun rememberVariants(
metaToolCode: String?,
secondLanguage: Language?,
eventSink: (Event) -> Unit,
onVariantSelected: (String) -> Unit,
): ImmutableList<ToolCard.State> {
if (metaToolCode == null) return persistentListOf()

val eventSink by rememberUpdatedState(eventSink)
val onVariantSelected by rememberUpdatedState(onVariantSelected)

return remember { toolsRepository.getNormalToolsFlow() }.collectAsState(emptyList()).value
.filter { it.metatoolCode == metaToolCode }
Expand All @@ -227,7 +203,7 @@ class ToolDetailsPresenter @AssistedInject constructor(
loadAvailableLanguages = true,
eventSink = {
when (it) {
ToolCard.Event.Click -> tool.code?.let { eventSink(Event.SwitchVariant(it)) }
ToolCard.Event.Click -> tool.code?.let { onVariantSelected(it) }
else -> Unit
}
}
Expand Down Expand Up @@ -263,6 +239,33 @@ class ToolDetailsPresenter @AssistedInject constructor(
}.collectAsState(persistentListOf()).value
}

private fun openTool(
tool: Tool?,
translation: Translation?,
secondTranslation: Translation?,
showTips: Boolean = false,
) {
tool?.let { tool ->
val intent = tool.createToolIntent(
context = context,
languages = when (tool.type) {
Tool.Type.ARTICLE -> listOfNotNull(
secondTranslation?.languageCode ?: translation?.languageCode
)

else -> listOfNotNull(translation?.languageCode, secondTranslation?.languageCode)
},
activeLocale = secondTranslation?.languageCode,
showTips = showTips
)

if (intent != null) {
eventBus.post(OpenAnalyticsActionEvent(ACTION_OPEN_TOOL, tool.code, SOURCE_TOOL_DETAILS))
navigator.goTo(IntentScreen(intent))
}
}
}

@AssistedFactory
@CircuitInject(ToolDetailsScreen::class, SingletonComponent::class)
interface Factory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ import org.cru.godtools.downloadmanager.DownloadProgress
import org.cru.godtools.model.Language
import org.cru.godtools.model.Tool
import org.cru.godtools.model.Translation
import org.cru.godtools.shared.tool.parser.model.Manifest
import org.cru.godtools.ui.drawer.DrawerMenuScreen
import org.cru.godtools.ui.tools.ToolCard

@Parcelize
data class ToolDetailsScreen(val initialTool: String, val secondLanguage: Locale? = null) : Screen {
data class State(
data class UiState(
val toolCode: String? = null,
val tool: Tool? = null,
val banner: File? = null,
Expand All @@ -35,21 +34,20 @@ data class ToolDetailsScreen(val initialTool: String, val secondLanguage: Locale
val availableLanguages: ImmutableList<String> = persistentListOf(),
val variants: ImmutableList<ToolCard.State> = persistentListOf(),
val drawerState: DrawerMenuScreen.State = DrawerMenuScreen.State(),
val eventSink: (Event) -> Unit = {},
val eventSink: (UiEvent) -> Unit = {},
) : CircuitUiState

enum class Page(@StringRes val tabLabel: Int) {
DESCRIPTION(R.string.label_tools_about),
VARIANTS(R.string.tool_details_section_variants_label)
}

sealed interface Event : CircuitUiEvent {
data object NavigateUp : Event
data object OpenTool : Event
data object OpenToolTraining : Event
data object PinTool : Event
data object UnpinTool : Event
data class SwitchVariant(val variant: String) : Event
data object PinShortcut : Event
sealed interface UiEvent : CircuitUiEvent {
data object NavigateUp : UiEvent
data object OpenTool : UiEvent
data object OpenToolTraining : UiEvent
data object PinTool : UiEvent
data object UnpinTool : UiEvent
data object PinShortcut : UiEvent
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import org.cru.godtools.model.Language
import org.cru.godtools.model.randomTool
import org.cru.godtools.model.randomTranslation
import org.cru.godtools.ui.drawer.DrawerMenuScreenStateTestData
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.State
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen.UiState
import org.cru.godtools.ui.tools.ToolCard
import org.cru.godtools.ui.tools.ToolCardStateTestData
import org.junit.runner.RunWith
Expand All @@ -43,7 +43,7 @@ class ToolDetailsLayoutPaparazziTest(
ToolCardStateTestData.tool.copy(secondLanguage = null)
)

private val state = State(
private val state = UiState(
tool = randomTool(
detailsBannerYoutubeVideoId = null,
shares = 123355,
Expand Down
Loading
Loading