@@ -23,8 +23,10 @@ import androidx.compose.animation.slideOutVertically
2323import androidx.compose.ui.graphics.graphicsLayer
2424import androidx.compose.foundation.ExperimentalFoundationApi
2525import androidx.compose.foundation.background
26+ import androidx.compose.foundation.border
2627import androidx.compose.foundation.clickable
2728import androidx.compose.foundation.combinedClickable
29+ import androidx.compose.foundation.interaction.MutableInteractionSource
2830import androidx.compose.foundation.gestures.detectVerticalDragGestures
2931import androidx.compose.ui.input.nestedscroll.nestedScroll
3032import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
@@ -2770,6 +2772,8 @@ private fun PasswordListContent(
27702772
27712773 // 堆叠展开状态 - 记录哪些分组已展开
27722774 var expandedGroups by remember { mutableStateOf(setOf<String >()) }
2775+ val outsideTapInteractionSource = remember { MutableInteractionSource () }
2776+ val canCollapseExpandedGroups = effectiveStackCardMode == StackCardMode .AUTO && expandedGroups.isNotEmpty()
27732777
27742778 // 当分组模式改变时,重置展开状态
27752779 LaunchedEffect (effectiveGroupMode, effectiveStackCardMode) {
@@ -3421,7 +3425,17 @@ private fun PasswordListContent(
34213425 }
34223426
34233427 // 密码列表 - 使用堆叠分组视图
3424- Box (modifier = Modifier .fillMaxSize()) {
3428+ Box (
3429+ modifier = Modifier
3430+ .fillMaxSize()
3431+ .clickable(
3432+ enabled = canCollapseExpandedGroups,
3433+ interactionSource = outsideTapInteractionSource,
3434+ indication = null
3435+ ) {
3436+ expandedGroups = emptySet()
3437+ }
3438+ ) {
34253439 if (passwordEntries.isEmpty() && searchQuery.isEmpty()) {
34263440 // Empty state with pull-to-search
34273441 Box (
@@ -5876,6 +5890,16 @@ private fun StackedPasswordGroup(
58765890 }
58775891 } else {
58785892 // --- 展开状态的内容 ---
5893+ val edgeInteractionSource = remember { MutableInteractionSource () }
5894+ val edgeContainerColor = MaterialTheme .colorScheme.surfaceVariant.copy(alpha = 0.52f )
5895+ val edgeBorderColor = MaterialTheme .colorScheme.outline.copy(alpha = 0.45f )
5896+ val edgeHitWidth = 14 .dp
5897+ val edgeHitHeight = 12 .dp
5898+ val edgeTapModifier = Modifier .clickable(
5899+ interactionSource = edgeInteractionSource,
5900+ indication = null ,
5901+ onClick = onToggleExpand
5902+ )
58795903 Column (
58805904 modifier = Modifier .fillMaxWidth()
58815905 ) {
@@ -5926,89 +5950,134 @@ private fun StackedPasswordGroup(
59265950 )
59275951
59285952 // 📦 2. 密码列表内容
5929- Column (
5930- modifier = Modifier .padding(16 .dp),
5931- verticalArrangement = Arrangement .spacedBy(12 .dp)
5953+ Box (
5954+ modifier = Modifier
5955+ .fillMaxWidth()
5956+ .padding(horizontal = 12 .dp, vertical = 10 .dp)
5957+ .clip(RoundedCornerShape (16 .dp))
5958+ .background(edgeContainerColor)
5959+ .border(
5960+ width = 1 .dp,
5961+ color = edgeBorderColor,
5962+ shape = RoundedCornerShape (16 .dp)
5963+ )
59325964 ) {
5933- val groupedByInfo = passwords.groupBy { getPasswordInfoKey(it) }
5934-
5935- groupedByInfo.values.forEachIndexed { groupIndex, passwordGroup ->
5936- // 列表项动画
5937- val itemEnterDelay = groupIndex * 30
5938- var isVisible by remember { mutableStateOf(false ) }
5939- LaunchedEffect (Unit ) {
5940- isVisible = true
5941- }
5965+ Column (
5966+ modifier = Modifier
5967+ .fillMaxWidth()
5968+ .padding(horizontal = edgeHitWidth, vertical = edgeHitHeight),
5969+ verticalArrangement = Arrangement .spacedBy(12 .dp)
5970+ ) {
5971+ val groupedByInfo = passwords.groupBy { getPasswordInfoKey(it) }
59425972
5943- AnimatedVisibility (
5944- visible = isVisible,
5945- enter = fadeIn(tween(300 , delayMillis = itemEnterDelay)) +
5946- androidx.compose.animation.slideInVertically(
5947- animationSpec = spring(stiffness = Spring .StiffnessMediumLow ),
5948- initialOffsetY = { 50 }
5949- ),
5950- modifier = Modifier .fillMaxWidth()
5951- ) {
5952- takagi.ru.monica.ui.gestures.SwipeActions (
5953- onSwipeLeft = { onSwipeLeft(passwordGroup.first()) },
5954- onSwipeRight = { onSwipeRight(passwordGroup.first()) },
5955- enabled = true
5973+ groupedByInfo.values.forEachIndexed { groupIndex, passwordGroup ->
5974+ // 列表项动画
5975+ val itemEnterDelay = groupIndex * 30
5976+ var isVisible by remember { mutableStateOf(false ) }
5977+ LaunchedEffect (Unit ) {
5978+ isVisible = true
5979+ }
5980+
5981+ AnimatedVisibility (
5982+ visible = isVisible,
5983+ enter = fadeIn(tween(300 , delayMillis = itemEnterDelay)) +
5984+ androidx.compose.animation.slideInVertically(
5985+ animationSpec = spring(stiffness = Spring .StiffnessMediumLow ),
5986+ initialOffsetY = { 50 }
5987+ ),
5988+ modifier = Modifier .fillMaxWidth()
59565989 ) {
5957- if (passwordGroup.size == 1 ) {
5958- val password = passwordGroup.first()
5959- PasswordEntryCard (
5960- entry = password,
5961- onClick = {
5962- if (isSelectionMode) {
5963- onToggleSelection(password.id)
5964- } else {
5965- onPasswordClick(password)
5966- }
5967- },
5968- onLongClick = { onLongClick(password) },
5969- onToggleFavorite = { onToggleFavorite(password) },
5970- onToggleGroupCover = if (passwords.size > 1 ) {
5971- { onToggleGroupCover(password) }
5972- } else null ,
5973- isSelectionMode = isSelectionMode,
5974- isSelected = selectedPasswords.contains(password.id),
5975- canSetGroupCover = passwords.size > 1 ,
5976- isInExpandedGroup = true , // We are inside the expanded container
5977- isSingleCard = false ,
5978- iconCardsEnabled = iconCardsEnabled,
5979- passwordCardDisplayMode = passwordCardDisplayMode,
5980- enableSharedBounds = enableSharedBounds
5981- )
5982- } else {
5983- MultiPasswordEntryCard (
5984- passwords = passwordGroup,
5985- onClick = { password ->
5986- if (isSelectionMode) {
5987- onToggleSelection(password.id)
5988- } else {
5989- onPasswordClick(password)
5990- }
5991- },
5992- onCardClick = if (! isSelectionMode) {
5993- { onOpenMultiPasswordDialog(passwordGroup) }
5994- } else null ,
5995- onLongClick = { onLongClick(passwordGroup.first()) },
5996- onToggleFavorite = { password -> onToggleFavorite(password) },
5997- onToggleGroupCover = if (passwords.size > 1 ) {
5998- { password -> onToggleGroupCover(password) }
5999- } else null ,
6000- isSelectionMode = isSelectionMode,
6001- selectedPasswords = selectedPasswords,
6002- canSetGroupCover = passwords.size > 1 ,
6003- hasGroupCover = hasGroupCover,
6004- isInExpandedGroup = true , // We are inside the expanded container
6005- iconCardsEnabled = iconCardsEnabled,
6006- passwordCardDisplayMode = passwordCardDisplayMode
6007- )
5990+ takagi.ru.monica.ui.gestures.SwipeActions (
5991+ onSwipeLeft = { onSwipeLeft(passwordGroup.first()) },
5992+ onSwipeRight = { onSwipeRight(passwordGroup.first()) },
5993+ enabled = true
5994+ ) {
5995+ if (passwordGroup.size == 1 ) {
5996+ val password = passwordGroup.first()
5997+ PasswordEntryCard (
5998+ entry = password,
5999+ onClick = {
6000+ if (isSelectionMode) {
6001+ onToggleSelection(password.id)
6002+ } else {
6003+ onPasswordClick(password)
6004+ }
6005+ },
6006+ onLongClick = { onLongClick(password) },
6007+ onToggleFavorite = { onToggleFavorite(password) },
6008+ onToggleGroupCover = if (passwords.size > 1 ) {
6009+ { onToggleGroupCover(password) }
6010+ } else null ,
6011+ isSelectionMode = isSelectionMode,
6012+ isSelected = selectedPasswords.contains(password.id),
6013+ canSetGroupCover = passwords.size > 1 ,
6014+ isInExpandedGroup = true , // We are inside the expanded container
6015+ isSingleCard = false ,
6016+ iconCardsEnabled = iconCardsEnabled,
6017+ passwordCardDisplayMode = passwordCardDisplayMode,
6018+ enableSharedBounds = enableSharedBounds
6019+ )
6020+ } else {
6021+ MultiPasswordEntryCard (
6022+ passwords = passwordGroup,
6023+ onClick = { password ->
6024+ if (isSelectionMode) {
6025+ onToggleSelection(password.id)
6026+ } else {
6027+ onPasswordClick(password)
6028+ }
6029+ },
6030+ onCardClick = if (! isSelectionMode) {
6031+ { onOpenMultiPasswordDialog(passwordGroup) }
6032+ } else null ,
6033+ onLongClick = { onLongClick(passwordGroup.first()) },
6034+ onToggleFavorite = { password -> onToggleFavorite(password) },
6035+ onToggleGroupCover = if (passwords.size > 1 ) {
6036+ { password -> onToggleGroupCover(password) }
6037+ } else null ,
6038+ isSelectionMode = isSelectionMode,
6039+ selectedPasswords = selectedPasswords,
6040+ canSetGroupCover = passwords.size > 1 ,
6041+ hasGroupCover = hasGroupCover,
6042+ isInExpandedGroup = true , // We are inside the expanded container
6043+ iconCardsEnabled = iconCardsEnabled,
6044+ passwordCardDisplayMode = passwordCardDisplayMode
6045+ )
6046+ }
60086047 }
60096048 }
60106049 }
60116050 }
6051+
6052+ // Expanded state edge zones: only these non-card areas collapse the stack.
6053+ Box (
6054+ modifier = Modifier
6055+ .align(Alignment .TopCenter )
6056+ .fillMaxWidth()
6057+ .height(edgeHitHeight)
6058+ .then(edgeTapModifier)
6059+ )
6060+ Box (
6061+ modifier = Modifier
6062+ .align(Alignment .BottomCenter )
6063+ .fillMaxWidth()
6064+ .height(edgeHitHeight)
6065+ .then(edgeTapModifier)
6066+ )
6067+ Box (
6068+ modifier = Modifier
6069+ .align(Alignment .CenterStart )
6070+ .width(edgeHitWidth)
6071+ .fillMaxHeight()
6072+ .then(edgeTapModifier)
6073+ )
6074+ Box (
6075+ modifier = Modifier
6076+ .align(Alignment .CenterEnd )
6077+ .width(edgeHitWidth)
6078+ .fillMaxHeight()
6079+ .then(edgeTapModifier)
6080+ )
60126081 }
60136082 }
60146083 }
0 commit comments