mirror of
https://github.com/bitwarden/android.git
synced 2025-02-22 16:49:13 +03:00
fubar
This commit is contained in:
parent
5b92e81823
commit
5145168a94
4 changed files with 92 additions and 48 deletions
app/src/main/java/com/x8bit/bitwarden/ui
platform/components/coachmark
vault/feature/addedit
|
@ -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)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue