diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 07476195c..cd111e2a1 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -55,6 +55,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app" // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no // NewPipeExtractor in the runtime path. -const val STRAW_VERSION_CODE = 45 -const val STRAW_VERSION_NAME = "0.1.0-BE" +const val STRAW_VERSION_CODE = 46 +const val STRAW_VERSION_NAME = "0.1.0-BF" const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt index 2461a8182..2170bb823 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt @@ -8,8 +8,10 @@ package com.sulkta.straw +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -78,6 +80,8 @@ import com.sulkta.straw.data.History import com.sulkta.straw.data.Subscriptions import com.sulkta.straw.data.WatchHistoryItem import com.sulkta.straw.feature.feed.SubscriptionFeedViewModel +import com.sulkta.straw.feature.playlist.VideoActionTarget +import com.sulkta.straw.feature.playlist.VideoActionsSheet import com.sulkta.straw.feature.search.StreamItem import com.sulkta.straw.OverlayDimColor import com.sulkta.straw.util.formatDuration @@ -234,6 +238,10 @@ fun StrawHome( @Composable private fun HistoryPane(onOpenVideo: (url: String, title: String) -> Unit) { val watches by History.get().watches.collectAsState() + var actionTarget by remember { mutableStateOf(null) } + actionTarget?.let { t -> + VideoActionsSheet(target = t, onDismiss = { actionTarget = null }) + } Column { Text( @@ -252,7 +260,18 @@ private fun HistoryPane(onOpenVideo: (url: String, title: String) -> Unit) { } else { LazyColumn { items(watches) { w -> - RecentRow(w) { onOpenVideo(w.url, w.title) } + RecentRow( + item = w, + onClick = { onOpenVideo(w.url, w.title) }, + onLongClick = { + actionTarget = VideoActionTarget( + streamUrl = w.url, + title = w.title, + uploader = w.uploader, + thumbnail = w.thumbnail, + ) + }, + ) HorizontalDivider() } } @@ -269,6 +288,10 @@ private fun SubsPane( val subs by Subscriptions.get().subs.collectAsState() val feed by feedVm.ui.collectAsState() val watches by History.get().watches.collectAsState() + var actionTarget by remember { mutableStateOf(null) } + actionTarget?.let { t -> + VideoActionsSheet(target = t, onDismiss = { actionTarget = null }) + } LaunchedEffect(subs) { feedVm.refreshIfStale() } // Filter + pagination state. hideWatched is sticky for the session @@ -415,7 +438,18 @@ private fun SubsPane( } LazyColumn(state = listState) { items(displayed) { item -> - FeedRow(item) { onOpenVideo(item.url, item.title) } + FeedRow( + item = item, + onClick = { onOpenVideo(item.url, item.title) }, + onLongClick = { + actionTarget = VideoActionTarget( + streamUrl = item.url, + title = item.title, + uploader = item.uploader, + thumbnail = item.thumbnail, + ) + }, + ) HorizontalDivider() } if (hasMore) { @@ -456,12 +490,17 @@ private val VIDEO_ID_RE = Regex("(?:v=|/)([A-Za-z0-9_-]{11})(?:[?&#].*)?$") private fun extractVideoId(url: String): String = VIDEO_ID_RE.find(url)?.groupValues?.getOrNull(1).orEmpty() +@OptIn(ExperimentalFoundationApi::class) @Composable -private fun FeedRow(item: StreamItem, onClick: () -> Unit) { +private fun FeedRow( + item: StreamItem, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) .padding(vertical = 8.dp), verticalAlignment = Alignment.Top, ) { @@ -586,12 +625,17 @@ private fun SubChip( } } +@OptIn(ExperimentalFoundationApi::class) @Composable -private fun RecentRow(item: WatchHistoryItem, onClick: () -> Unit) { +private fun RecentRow( + item: WatchHistoryItem, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt index 9d3e57143..54e41dd0f 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt @@ -5,7 +5,9 @@ package com.sulkta.straw.feature.channel +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -41,11 +43,16 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.sulkta.straw.data.ChannelRef import com.sulkta.straw.data.Subscriptions +import com.sulkta.straw.feature.playlist.VideoActionTarget +import com.sulkta.straw.feature.playlist.VideoActionsSheet import com.sulkta.straw.feature.search.StreamItem import com.sulkta.straw.util.formatCount import com.sulkta.straw.util.formatDuration @@ -61,6 +68,10 @@ fun ChannelScreen( LaunchedEffect(channelUrl) { vm.load(channelUrl) } val subs by Subscriptions.get().subs.collectAsState() val subscribed = subs.any { it.url == channelUrl } + var actionTarget by remember { mutableStateOf(null) } + actionTarget?.let { t -> + VideoActionsSheet(target = t, onDismiss = { actionTarget = null }) + } when { state.loading -> Box( @@ -130,19 +141,35 @@ fun ChannelScreen( HorizontalDivider() } items(state.videos) { item -> - ChannelVideoRow(item) { onOpenVideo(item.url, item.title) } + ChannelVideoRow( + item = item, + onClick = { onOpenVideo(item.url, item.title) }, + onLongClick = { + actionTarget = VideoActionTarget( + streamUrl = item.url, + title = item.title, + uploader = item.uploader, + thumbnail = item.thumbnail, + ) + }, + ) HorizontalDivider() } } } } +@OptIn(ExperimentalFoundationApi::class) @Composable -private fun ChannelVideoRow(item: StreamItem, onClick: () -> Unit) { +private fun ChannelVideoRow( + item: StreamItem, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) .padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.Top, ) { diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt index b2209bcc0..8cb277a6c 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt @@ -17,8 +17,10 @@ import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState @@ -92,7 +94,7 @@ import coil3.compose.AsyncImage import com.sulkta.straw.OverlayChromeColor import com.sulkta.straw.OverlayDimColor import com.sulkta.straw.data.PlaylistItem -import com.sulkta.straw.data.Playlists +import com.sulkta.straw.feature.playlist.SaveToPlaylistDialog import com.sulkta.straw.feature.download.DownloadKind import com.sulkta.straw.feature.download.Downloader import com.sulkta.straw.feature.player.LocalStrawController @@ -123,6 +125,13 @@ fun VideoDetailScreen( val activity = context as? Activity var showDownloadDialog by remember { mutableStateOf(false) } var showSaveToPlaylistDialog by remember { mutableStateOf(false) } + var actionTarget by remember { mutableStateOf(null) } + actionTarget?.let { t -> + com.sulkta.straw.feature.playlist.VideoActionsSheet( + target = t, + onDismiss = { actionTarget = null }, + ) + } // Inline-play state resets when navigating to a different video. // BUT: if the shared MediaController is already playing this exact // stream — most commonly because the user popped back from @@ -536,7 +545,18 @@ fun VideoDetailScreen( ) Spacer(modifier = Modifier.height(8.dp)) d.related.take(20).forEach { rel -> - RelatedRow(rel) { onOpenVideo(rel.url, rel.title) } + RelatedRow( + item = rel, + onClick = { onOpenVideo(rel.url, rel.title) }, + onLongClick = { + actionTarget = com.sulkta.straw.feature.playlist.VideoActionTarget( + streamUrl = rel.url, + title = rel.title, + uploader = rel.uploader, + thumbnail = rel.thumbnail, + ) + }, + ) HorizontalDivider() } } @@ -551,7 +571,18 @@ fun VideoDetailScreen( ) Spacer(modifier = Modifier.height(8.dp)) d.moreFromChannel.take(20).forEach { item -> - RelatedRow(item) { onOpenVideo(item.url, item.title) } + RelatedRow( + item = item, + onClick = { onOpenVideo(item.url, item.title) }, + onLongClick = { + actionTarget = com.sulkta.straw.feature.playlist.VideoActionTarget( + streamUrl = item.url, + title = item.title, + uploader = item.uploader, + thumbnail = item.thumbnail, + ) + }, + ) HorizontalDivider() } } @@ -629,15 +660,17 @@ fun VideoDetailScreen( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun RelatedRow( item: StreamItem, onClick: () -> Unit, + onLongClick: () -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) .padding(vertical = 8.dp), verticalAlignment = Alignment.Top, ) { @@ -676,90 +709,6 @@ private fun RelatedRow( } } -@Composable -private fun SaveToPlaylistDialog( - item: PlaylistItem, - onDismiss: () -> Unit, -) { - val context = LocalContext.current - val store = Playlists.get() - val playlists by store.playlists.collectAsState() - var creatingNew by remember { mutableStateOf(false) } - var newName by remember { mutableStateOf("") } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Save to playlist") }, - text = { - Column { - if (playlists.isEmpty() && !creatingNew) { - Text( - "No playlists yet. Create one to save this video.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(8.dp)) - } - playlists.forEach { pl -> - val already = pl.items.any { it.streamUrl == item.streamUrl } - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = !already) { - store.addItem(pl.id, item) - Toast.makeText(context, "saved to ${pl.name}", Toast.LENGTH_SHORT).show() - onDismiss() - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text(if (already) "✓" else "○", modifier = Modifier.width(28.dp)) - Column(modifier = Modifier.weight(1f)) { - Text(pl.name, style = MaterialTheme.typography.bodyLarge) - Text( - "${pl.items.size} video${if (pl.items.size == 1) "" else "s"}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - HorizontalDivider() - } - if (creatingNew) { - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = newName, - onValueChange = { newName = it }, - label = { Text("New playlist name") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - Spacer(modifier = Modifier.height(8.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = { - val pl = store.create(newName) - store.addItem(pl.id, item) - Toast.makeText(context, "created ${pl.name} + saved", Toast.LENGTH_SHORT).show() - onDismiss() - }) { Text("Create + save") } - OutlinedButton(onClick = { creatingNew = false; newName = "" }) { - Text("Cancel") - } - } - } else { - Spacer(modifier = Modifier.height(12.dp)) - OutlinedButton(onClick = { creatingNew = true }) { - Text("+ New playlist") - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text("Close") } - }, - ) -} - /** * Inline player surface inside VideoDetail's 16:9 thumbnail box. Renders * a PlayerView bound to the shared LocalStrawController — the same diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/playlist/VideoActions.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/playlist/VideoActions.kt new file mode 100644 index 000000000..e57433666 --- /dev/null +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/playlist/VideoActions.kt @@ -0,0 +1,256 @@ +/* + * SPDX-FileCopyrightText: 2026 Sulkta-Coop + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Shared long-press actions surface for video rows. The menu shows + * "Save to playlist" + "Share" (and Add-to-Queue later when the queue + * substrate lands). Every video row in the app — Search results, + * Subs feed, Channel videos, History, Related — calls + * `showVideoActions(...)` from a `combinedClickable.onLongClick`. + * + * Pure-Compose surface — no ViewModel needed; PlaylistsStore is a + * process-wide singleton and the share Intent is a fire-and-forget + * Android system action. + */ + +package com.sulkta.straw.feature.playlist + +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlaylistAdd +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.sulkta.straw.data.PlaylistItem +import com.sulkta.straw.data.Playlists + +/** + * Minimal video descriptor for the actions sheet. Avoids dragging + * the full search.StreamItem (which has extractor fields the + * actions don't need) so the same surface can be invoked from + * history rows where we only have a WatchHistoryItem. + */ +data class VideoActionTarget( + val streamUrl: String, + val title: String, + val uploader: String, + val thumbnail: String?, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun VideoActionsSheet( + target: VideoActionTarget, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val sheetState = rememberModalBottomSheetState() + var showSaveDialog by remember { mutableStateOf(false) } + + if (showSaveDialog) { + SaveToPlaylistDialog( + item = PlaylistItem( + streamUrl = target.streamUrl, + title = target.title, + thumbnail = target.thumbnail, + uploader = target.uploader, + ), + onDismiss = { + showSaveDialog = false + onDismiss() + }, + ) + return + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) { + // Title row — truncated to one line, gives context for the + // actions below. + Text( + text = target.title, + style = MaterialTheme.typography.titleSmall, + maxLines = 2, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), + ) + if (target.uploader.isNotBlank()) { + Text( + text = target.uploader, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 0.dp), + ) + } + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider() + ActionRow( + icon = Icons.Filled.PlaylistAdd, + label = "Save to playlist", + onClick = { showSaveDialog = true }, + ) + ActionRow( + icon = Icons.Filled.Share, + label = "Share", + onClick = { + val send = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, target.streamUrl) + putExtra(Intent.EXTRA_SUBJECT, target.title) + } + context.startActivity( + Intent.createChooser(send, "Share video").addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK, + ), + ) + onDismiss() + }, + ) + } + } +} + +@Composable +private fun ActionRow( + icon: androidx.compose.ui.graphics.vector.ImageVector, + label: String, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 20.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.width(16.dp)) + Text(text = label, style = MaterialTheme.typography.bodyLarge) + } +} + +/** + * Shared "Save to playlist" dialog — was previously inline in + * VideoDetailScreen; promoted to its own file so the long-press + * menu on any row can reuse it. + */ +@Composable +fun SaveToPlaylistDialog( + item: PlaylistItem, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val store = Playlists.get() + val playlists by store.playlists.collectAsState() + var creatingNew by remember { mutableStateOf(false) } + var newName by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Save to playlist") }, + text = { + Column { + if (playlists.isEmpty() && !creatingNew) { + Text( + "No playlists yet. Create one to save this video.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + playlists.forEach { pl -> + val already = pl.items.any { it.streamUrl == item.streamUrl } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = !already) { + store.addItem(pl.id, item) + Toast.makeText(context, "saved to ${pl.name}", Toast.LENGTH_SHORT).show() + onDismiss() + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(if (already) "✓" else "○", modifier = Modifier.width(28.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(pl.name, style = MaterialTheme.typography.bodyLarge) + Text( + "${pl.items.size} video${if (pl.items.size == 1) "" else "s"}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + HorizontalDivider() + } + if (creatingNew) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = newName, + onValueChange = { newName = it }, + label = { Text("New playlist name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + Spacer(modifier = Modifier.height(8.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = { + val pl = store.create(newName) + store.addItem(pl.id, item) + Toast.makeText(context, "created ${pl.name} + saved", Toast.LENGTH_SHORT).show() + onDismiss() + }) { Text("Create + save") } + OutlinedButton(onClick = { creatingNew = false; newName = "" }) { + Text("Cancel") + } + } + } else { + Spacer(modifier = Modifier.height(12.dp)) + OutlinedButton(onClick = { creatingNew = true }) { + Text("+ New playlist") + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { Text("Close") } + }, + ) +} + diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt index 3a8b94ea2..3c9336fdd 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt @@ -5,7 +5,9 @@ package com.sulkta.straw.feature.search +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -44,6 +46,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.sulkta.straw.data.History +import com.sulkta.straw.feature.playlist.VideoActionTarget +import com.sulkta.straw.feature.playlist.VideoActionsSheet +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.sulkta.straw.util.formatDuration import com.sulkta.straw.util.formatViews @@ -55,6 +61,10 @@ fun SearchScreen( ) { val state by vm.ui.collectAsStateWithLifecycle() val recentSearches by History.get().searches.collectAsState() + var actionTarget by remember { mutableStateOf(null) } + actionTarget?.let { target -> + VideoActionsSheet(target = target, onDismiss = { actionTarget = null }) + } Column(modifier = Modifier.fillMaxSize().statusBarsPadding().padding(16.dp)) { OutlinedTextField( @@ -153,6 +163,14 @@ fun SearchScreen( ResultRow( item = item, onClick = { onOpenVideo(item.url, item.title) }, + onLongClick = { + actionTarget = VideoActionTarget( + streamUrl = item.url, + title = item.title, + uploader = item.uploader, + thumbnail = item.thumbnail, + ) + }, onChannelClick = { url -> onOpenChannel(url, item.uploader) }, ) HorizontalDivider() @@ -163,16 +181,18 @@ fun SearchScreen( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun ResultRow( item: StreamItem, onClick: () -> Unit, + onLongClick: () -> Unit, onChannelClick: (url: String) -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) .padding(vertical = 10.dp), verticalAlignment = Alignment.Top, ) {