1
0
Fork 0
mirror of https://github.com/bitwarden/android.git synced 2025-02-22 16:49:13 +03:00
This commit is contained in:
Dave Severns 2025-01-10 16:00:01 -05:00
parent 5b92e81823
commit 5145168a94
4 changed files with 92 additions and 48 deletions
app/src/main/java/com/x8bit/bitwarden/ui

View file

@ -11,20 +11,26 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -35,6 +41,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconBu
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -61,55 +68,68 @@ fun <T : Enum<T>> CoachMarkContainer(
content: @Composable CoachMarkScope<T>.() -> Unit, content: @Composable CoachMarkScope<T>.() -> Unit,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Box(modifier = Modifier Box(
.fillMaxSize() modifier = Modifier
.then(modifier), .fillMaxSize()
.then(modifier),
) { ) {
Timber.i("Coach: I am the full container") Timber.i("Coach: I am the full container")
CoachMarkScopeInstance(coachMarkState = state).content() CoachMarkScopeInstance(coachMarkState = state).content()
if ( val boundedRectangle by state.currentHighlightBounds
state.currentHighlightBounds.value != Rect.Zero && state.isVisible.value val isVisible by state.isVisible
) { val currentHighlightShape by state.currentHighlightShape
Timber.i("Coach do I get called? Im trying draw the overlay") val highlightPath =
val boundedRectangle by state.currentHighlightBounds remember(boundedRectangle, currentHighlightShape) {
val highlightArea = Rect( if (boundedRectangle == Rect.Zero) {
topLeft = boundedRectangle.topLeft, Timber.w("Coach: highlightPath is Rect.Zero")
bottomRight = boundedRectangle.bottomRight, return@remember Path()
) }
val highlightPath = Path().apply { val highlightArea = Rect(
topLeft = boundedRectangle.topLeft,
bottomRight = boundedRectangle.bottomRight,
)
Timber.i("Coach: I am applying the path for $highlightArea") Timber.i("Coach: I am applying the path for $highlightArea")
when (state.currentHighlightShape.value) { Path().apply {
CoachMarkHighlightShape.SQUARE -> addRoundRect( when (currentHighlightShape) {
RoundRect( CoachMarkHighlightShape.SQUARE -> addRoundRect(
rect = highlightArea, RoundRect(
cornerRadius = CornerRadius( rect = highlightArea,
x = ROUNDED_RECT_RADIUS, cornerRadius = CornerRadius(
x = ROUNDED_RECT_RADIUS,
),
), ),
), )
)
CoachMarkHighlightShape.OVAL -> addOval(highlightArea) CoachMarkHighlightShape.OVAL -> addOval(highlightArea)
}
} }
} }
if (boundedRectangle != Rect.Zero && isVisible) {
Timber.i("Coach do I get called? Im trying draw the overlay")
val backgroundColor = BitwardenTheme.colorScheme.text.primary val backgroundColor = BitwardenTheme.colorScheme.text.primary
Box( Box(
modifier = Modifier modifier = Modifier
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { onTap = {
if (state.isVisible.value) { scope.launch {
scope.launch { Timber.i("Coach calling it in gestures")
Timber.i("Coach calling it in gestures") state.showToolTipForCurrentCoachMark()
state.showToolTipForCurrentCoachMark()
}
} }
}, },
) )
} }
.fillMaxSize() .fillMaxSize()
// .background(
// color = backgroundColor.copy(alpha = .75f)
// )
// .clip(
// CircleShape
// )
.drawBehind { .drawBehind {
Timber.i("Coach: in drawbehind, $highlightPath based on $boundedRectangle")
clipPath( clipPath(
path = highlightPath, path = highlightPath,
clipOp = ClipOp.Difference, clipOp = ClipOp.Difference,
@ -123,17 +143,17 @@ fun <T : Enum<T>> CoachMarkContainer(
}, },
) )
} }
} LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) {
LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) { if (state.currentHighlightBounds.value != Rect.Zero) {
if (state.currentHighlightBounds.value != Rect.Zero) { Timber.i("Coach: bounds changed do I get called? Im trying to show the tooltip")
Timber.i("Coach: bounds changed do I get called? Im trying to show the tooltip") state.showToolTipForCurrentCoachMark()
state.showToolTipForCurrentCoachMark() }
} }
} LaunchedEffect(Unit) {
LaunchedEffect(Unit) { if (state.isVisible.value && (state.currentHighlight.value != null)) {
if (state.isVisible.value && (state.currentHighlight.value != null)) { Timber.i("Coach calling it in Launched effect cause state is visible")
Timber.i("Coach calling it in Launched effect cause state is visible") state.showCoachMark(state.currentHighlight.value)
state.showCoachMark(state.currentHighlight.value) }
} }
} }
} }

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -199,6 +200,7 @@ private fun TooltipScope.CoachMarkToolTip(
rightAction: (@Composable RowScope.() -> Unit)?, rightAction: (@Composable RowScope.() -> Unit)?,
) { ) {
RichTooltip( RichTooltip(
modifier = Modifier.padding(start = 8.dp, end = 2.dp),
caretSize = DpSize(width = 24.dp, height = 16.dp), caretSize = DpSize(width = 24.dp, height = 16.dp),
title = { title = {
Row( Row(

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.ui.platform.components.coachmark package com.x8bit.bitwarden.ui.platform.components.coachmark
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TooltipState import androidx.compose.material3.TooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -9,9 +11,12 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.key.key
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
/** /**
* Manages the state of a coach mark sequence, guiding users through a series of highlights. * Manages the state of a coach mark sequence, guiding users through a series of highlights.
@ -92,7 +97,6 @@ class CoachMarkState<T : Enum<T>>(
Timber.i("Coach: I have been requested to show the highlight for ${highlightToShow?.key} with bounds: ${currentHighlightBounds.value}") Timber.i("Coach: I have been requested to show the highlight for ${highlightToShow?.key} with bounds: ${currentHighlightBounds.value}")
if (highlightToShow != null) { if (highlightToShow != null) {
updateCoachMarkStateInternal(highlightToShow) updateCoachMarkStateInternal(highlightToShow)
_isVisible.value = true
} else { } else {
showNextCoachMark() showNextCoachMark()
} }
@ -112,7 +116,6 @@ class CoachMarkState<T : Enum<T>>(
val index = orderedList.indexOf(previousHighlight?.key) val index = orderedList.indexOf(previousHighlight?.key)
if (index < 0 && previousHighlight != null) return if (index < 0 && previousHighlight != null) return
_currentHighlight.value = orderedList.getOrNull(index + 1) _currentHighlight.value = orderedList.getOrNull(index + 1)
_isVisible.value = currentHighlight.value != null
getCurrentHighlight() getCurrentHighlight()
} }
Timber.i("Coach: I have been requested to show next highlight which is: ${highlightToShow?.key} with bounds: ${highlightToShow?.highlightBounds}") Timber.i("Coach: I have been requested to show next highlight which is: ${highlightToShow?.key} with bounds: ${highlightToShow?.highlightBounds}")
@ -135,7 +138,6 @@ class CoachMarkState<T : Enum<T>>(
return return
} }
_currentHighlight.value = orderedList.getOrNull(index - 1) _currentHighlight.value = orderedList.getOrNull(index - 1)
_isVisible.value = this.currentHighlight.value != null
getCurrentHighlight() getCurrentHighlight()
} }
updateCoachMarkStateInternal(highlightToShow) updateCoachMarkStateInternal(highlightToShow)
@ -162,6 +164,7 @@ class CoachMarkState<T : Enum<T>>(
} }
private fun updateCoachMarkStateInternal(highlight: CoachMarkHighlightState<T>?) { private fun updateCoachMarkStateInternal(highlight: CoachMarkHighlightState<T>?) {
_isVisible.value = highlight != null
Timber.i("Coach: I have updated the shape and bounds, the new bounds are ${highlight?.highlightBounds}") Timber.i("Coach: I have updated the shape and bounds, the new bounds are ${highlight?.highlightBounds}")
_currentHighlightShape.value = highlight?.shape ?: CoachMarkHighlightShape.SQUARE _currentHighlightShape.value = highlight?.shape ?: CoachMarkHighlightShape.SQUARE
if (currentHighlightBounds.value != highlight?.highlightBounds) { if (currentHighlightBounds.value != highlight?.highlightBounds) {
@ -170,7 +173,7 @@ class CoachMarkState<T : Enum<T>>(
} }
} }
internal suspend fun showToolTipForCurrentCoachMark() { suspend fun showToolTipForCurrentCoachMark() {
Timber.i("Coach: I am trying to show") Timber.i("Coach: I am trying to show")
val currentCoachMark = mutex.withLock { val currentCoachMark = mutex.withLock {
getCurrentHighlight() getCurrentHighlight()
@ -180,6 +183,29 @@ class CoachMarkState<T : Enum<T>>(
currentCoachMark?.toolTipState?.show() currentCoachMark?.toolTipState?.show()
} }
suspend fun scrollUpToKey(
listState: LazyListState,
targetKey: T,
) {
val scrollAmount = (-1).toFloat()
var found = false
while (!found) {
val layoutInfo = listState.layoutInfo
val visibleItems = layoutInfo.visibleItemsInfo
if (visibleItems.any { it.key == targetKey }) {
found = true
} else {
if (listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0) {
// Reached the start of the list without finding the key
println("Key $targetKey not found")
found = true
} else {
listState.scrollBy(scrollAmount)
}
}
}
}
/** /**
* Cleans up the tooltip state by dismissing it if visible and calling onDispose. * Cleans up the tooltip state by dismissing it if visible and calling onDispose.
*/ */

View file

@ -258,12 +258,6 @@ fun VaultAddEditScreen(
orderedList = AddEditItemCoachMark.entries, orderedList = AddEditItemCoachMark.entries,
) )
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
delay(3000L)
if (coachMarkState.isVisible.value.not()) {
coachMarkState.showCoachMark(AddEditItemCoachMark.GENERATE_PASSWORD)
}
}
CoachMarkContainer( CoachMarkContainer(
state = coachMarkState, state = coachMarkState,
) { ) {
@ -287,7 +281,9 @@ fun VaultAddEditScreen(
BitwardenTextButton( BitwardenTextButton(
label = stringResource(id = R.string.save), label = stringResource(id = R.string.save),
onClick = remember(viewModel) { onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) } { coroutineScope.launch {
coachMarkState.showCoachMark()
}}
}, },
modifier = Modifier.testTag("SaveButton"), modifier = Modifier.testTag("SaveButton"),
) )