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.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -53,17 +54,20 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.input.pointer.pointerInput
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -178,6 +182,18 @@ fun LibraryScreen(
Icon(Icons.Default.Shuffle, contentDescription = "Shuffle play") 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( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.background, 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 -> tabLabels.forEachIndexed { index, label ->
Tab( Tab(
// Long-press detected at the pointer level: putting modifier = if (index == 0) Modifier else Modifier.pointerInput(index) {
// combinedClickable here would be masked by Tab's own awaitEachGesture {
// selectable semantics and break selection entirely. val down = awaitFirstDown(
modifier = Modifier.pointerInput(index) { requireUnconsumed = false,
detectTapGestures( pass = PointerEventPass.Initial,
onLongPress = {
if (index > 0) playlistToDelete = filteredPlaylists[index - 1]
},
) )
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, selected = selectedTabIndex == index,
onClick = { onClick = {