fix: folder tab long-press via Initial-pass pointerInput watcher + shared Handler timer; taps still select (verified on-device: 8-step gesture suite)

This commit is contained in:
avi 2026-09-07 13:57:49 -05:00
commit a99d4c43f4

View file

@ -4,7 +4,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -53,17 +54,20 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -178,6 +182,18 @@ fun LibraryScreen(
Icon(Icons.Default.Shuffle, contentDescription = "Shuffle play")
}
}
// Reliable delete path for the selected folder (besides long-press).
if (uiState.selectedPlaylistId != null) {
allPlaylists.firstOrNull { it.id == uiState.selectedPlaylistId }?.let { selected ->
IconButton(onClick = { playlistToDelete = selected }) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete folder",
tint = PremiumTextSecondary,
)
}
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.background,
@ -208,17 +224,38 @@ fun LibraryScreen(
}
},
) {
// One shared long-press timer for the folder tabs. The
// per-Tab watcher (pointerInput on the Initial pass) arms
// it on DOWN and disarms on UP/CANCEL/move; the Tab's own
// selectable keeps handling taps normally on the Main pass.
val tabLpHandler = android.os.Handler(android.os.Looper.getMainLooper())
DisposableEffect(Unit) {
onDispose { tabLpHandler.removeCallbacksAndMessages(null) }
}
tabLabels.forEachIndexed { index, label ->
Tab(
// Long-press detected at the pointer level: putting
// combinedClickable here would be masked by Tab's own
// selectable semantics and break selection entirely.
modifier = Modifier.pointerInput(index) {
detectTapGestures(
onLongPress = {
if (index > 0) playlistToDelete = filteredPlaylists[index - 1]
},
modifier = if (index == 0) Modifier else Modifier.pointerInput(index) {
awaitEachGesture {
val down = awaitFirstDown(
requireUnconsumed = false,
pass = PointerEventPass.Initial,
)
val playlist = filteredPlaylists[index - 1]
val slop = viewConfiguration.touchSlop
val timer = Runnable { playlistToDelete = playlist }
tabLpHandler.removeCallbacks(timer)
tabLpHandler.postDelayed(
timer,
viewConfiguration.longPressTimeoutMillis,
)
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
val change = event.changes.firstOrNull { it.id == down.id }
if (change == null || !change.pressed) break
if ((change.position - down.position).getDistance() > slop) break
}
tabLpHandler.removeCallbacks(timer)
}
},
selected = selectedTabIndex == index,
onClick = {