1
0
Fork 0
mirror of https://github.com/bitwarden/android.git synced 2025-01-30 19:53:47 +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.padding
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.input.pointer.pointerInput
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.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
@ -61,55 +68,68 @@ fun <T : Enum<T>> CoachMarkContainer(
content: @Composable CoachMarkScope<T>.() -> Unit,
) {
val scope = rememberCoroutineScope()
Box(modifier = Modifier
.fillMaxSize()
.then(modifier),
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
) {
Timber.i("Coach: I am the full container")
CoachMarkScopeInstance(coachMarkState = state).content()
if (
state.currentHighlightBounds.value != Rect.Zero && state.isVisible.value
) {
val boundedRectangle by state.currentHighlightBounds
val isVisible by state.isVisible
val currentHighlightShape by state.currentHighlightShape
Timber.i("Coach do I get called? Im trying draw the overlay")
val boundedRectangle by state.currentHighlightBounds
val highlightArea = Rect(
topLeft = boundedRectangle.topLeft,
bottomRight = boundedRectangle.bottomRight,
)
val highlightPath = Path().apply {
val highlightPath =
remember(boundedRectangle, currentHighlightShape) {
if (boundedRectangle == Rect.Zero) {
Timber.w("Coach: highlightPath is Rect.Zero")
return@remember Path()
}
val highlightArea = Rect(
topLeft = boundedRectangle.topLeft,
bottomRight = boundedRectangle.bottomRight,
)
Timber.i("Coach: I am applying the path for $highlightArea")
when (state.currentHighlightShape.value) {
CoachMarkHighlightShape.SQUARE -> addRoundRect(
RoundRect(
rect = highlightArea,
cornerRadius = CornerRadius(
x = ROUNDED_RECT_RADIUS,
Path().apply {
when (currentHighlightShape) {
CoachMarkHighlightShape.SQUARE -> addRoundRect(
RoundRect(
rect = highlightArea,
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
Box(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (state.isVisible.value) {
scope.launch {
Timber.i("Coach calling it in gestures")
state.showToolTipForCurrentCoachMark()
}
scope.launch {
Timber.i("Coach calling it in gestures")
state.showToolTipForCurrentCoachMark()
}
},
)
}
.fillMaxSize()
// .background(
// color = backgroundColor.copy(alpha = .75f)
// )
// .clip(
// CircleShape
// )
.drawBehind {
Timber.i("Coach: in drawbehind, $highlightPath based on $boundedRectangle")
clipPath(
path = highlightPath,
clipOp = ClipOp.Difference,
@ -123,17 +143,17 @@ fun <T : Enum<T>> CoachMarkContainer(
},
)
}
}
LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) {
if (state.currentHighlightBounds.value != Rect.Zero) {
Timber.i("Coach: bounds changed do I get called? Im trying to show the tooltip")
state.showToolTipForCurrentCoachMark()
LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) {
if (state.currentHighlightBounds.value != Rect.Zero) {
Timber.i("Coach: bounds changed do I get called? Im trying to show the tooltip")
state.showToolTipForCurrentCoachMark()
}
}
}
LaunchedEffect(Unit) {
if (state.isVisible.value && (state.currentHighlight.value != null)) {
Timber.i("Coach calling it in Launched effect cause state is visible")
state.showCoachMark(state.currentHighlight.value)
LaunchedEffect(Unit) {
if (state.isVisible.value && (state.currentHighlight.value != null)) {
Timber.i("Coach calling it in Launched effect cause state is visible")
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
@ -199,6 +200,7 @@ private fun TooltipScope.CoachMarkToolTip(
rightAction: (@Composable RowScope.() -> Unit)?,
) {
RichTooltip(
modifier = Modifier.padding(start = 8.dp, end = 2.dp),
caretSize = DpSize(width = 24.dp, height = 16.dp),
title = {
Row(

View file

@ -1,5 +1,7 @@
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.TooltipState
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.rememberSaveable
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.withLock
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
/**
* 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}")
if (highlightToShow != null) {
updateCoachMarkStateInternal(highlightToShow)
_isVisible.value = true
} else {
showNextCoachMark()
}
@ -112,7 +116,6 @@ class CoachMarkState<T : Enum<T>>(
val index = orderedList.indexOf(previousHighlight?.key)
if (index < 0 && previousHighlight != null) return
_currentHighlight.value = orderedList.getOrNull(index + 1)
_isVisible.value = currentHighlight.value != null
getCurrentHighlight()
}
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
}
_currentHighlight.value = orderedList.getOrNull(index - 1)
_isVisible.value = this.currentHighlight.value != null
getCurrentHighlight()
}
updateCoachMarkStateInternal(highlightToShow)
@ -162,6 +164,7 @@ class CoachMarkState<T : Enum<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}")
_currentHighlightShape.value = highlight?.shape ?: CoachMarkHighlightShape.SQUARE
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")
val currentCoachMark = mutex.withLock {
getCurrentHighlight()
@ -180,6 +183,29 @@ class CoachMarkState<T : Enum<T>>(
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.
*/

View file

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