Straw phase N: share + playback speed + audio-only toggle (v0.1.0-N / vc=2)

Player overlay (top-right) now hosts four buttons in a row:
- Speed: 1× by default; tap opens a dialog of 0.25× / 0.5× / 0.75× / 1× /
  1.25× / 1.5× / 1.75× / 2×. Applies via exoPlayer.playbackParameters.
- Audio-only toggle (📻/📺): toggles Player.TRACK_TYPE_VIDEO via
  TrackSelectionParameters. Saves bandwidth + battery for screen-off
  listening. Toast confirms state.
- Share (↗): Intent.ACTION_SEND with text/plain containing the YouTube
  URL + the video title as EXTRA_SUBJECT. Hands off to Android share
  sheet.
- PiP (⊟): same as M-1 but now part of the row instead of its own
  floating square.

VideoDetail screen: Play button now lives in a Row with an OutlinedButton
"Share" that fires the same ACTION_SEND chooser. Same UX surface for
users who land on detail without going to player.

Version: STRAW_VERSION_CODE 1 → 2, STRAW_VERSION_NAME "0.1.0-day1" →
"0.1.0-N" so the F-Droid client sees this as an upgrade.

Phase O next (per Cobb's "where are all the features"): quality picker,
related videos on detail, download (audio + video).
This commit is contained in:
Kayos 2026-05-23 21:20:15 -07:00
parent 7894fe5a4d
commit 253c5e268b
3 changed files with 137 additions and 27 deletions

View file

@ -15,6 +15,6 @@ const val NEWPIPE_APPLICATION_ID_OLD = "org.schabi.newpipe"
const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app"
// Sulkta fork — Straw
const val STRAW_VERSION_CODE = 1
const val STRAW_VERSION_NAME = "0.1.0-day1"
const val STRAW_VERSION_CODE = 2
const val STRAW_VERSION_NAME = "0.1.0-N"
const val STRAW_APPLICATION_ID = "com.sulkta.straw"

View file

@ -28,7 +28,10 @@ import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import android.content.Intent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
@ -56,6 +59,7 @@ fun VideoDetailScreen(
vm: VideoDetailViewModel = viewModel(),
) {
val state by vm.ui.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(streamUrl) { vm.load(streamUrl) }
Column(
@ -160,7 +164,17 @@ fun VideoDetailScreen(
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onPlay) { Text("Play") }
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = onPlay) { Text("Play") }
OutlinedButton(onClick = {
val send = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, streamUrl)
putExtra(Intent.EXTRA_SUBJECT, d.title)
}
context.startActivity(Intent.createChooser(send, "Share video"))
}) { Text("Share") }
}
Spacer(modifier = Modifier.height(16.dp))
Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)

View file

@ -10,6 +10,7 @@ package com.sulkta.straw.feature.player
import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Intent
import android.os.Build
import android.util.Rational
import android.widget.Toast
@ -21,14 +22,24 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
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.draw.clip
@ -44,7 +55,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.TrackGroup as Media3TrackGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
@ -70,6 +84,11 @@ fun PlayerScreen(
val state by vm.ui.collectAsStateWithLifecycle()
LaunchedEffect(streamUrl) { vm.resolve(streamUrl) }
// Local UI state for speed / audio-only / dialog open.
var playbackSpeed by remember { mutableStateOf(1.0f) }
var audioOnly by remember { mutableStateOf(false) }
var showSpeedDialog by remember { mutableStateOf(false) }
val exoPlayer = remember {
ExoPlayer.Builder(context)
.setAudioAttributes(
@ -237,31 +256,58 @@ fun PlayerScreen(
)
}
}
// PiP button — top-right. Tapping it puts the player into
// floating-window mode so the user can use other apps while
// the video keeps playing.
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(12.dp)
.size(36.dp)
.clip(RoundedCornerShape(6.dp))
.background(Color(0xCC222222))
.clickable {
val activity = (context as? Activity) ?: return@clickable
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
runCatching { activity.enterPictureInPictureMode(params) }
}
},
contentAlignment = Alignment.Center,
// Top-right overlay — speed / audio-only / share / PiP.
Row(
modifier = Modifier.align(Alignment.TopEnd).padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = "",
color = Color.White,
style = MaterialTheme.typography.titleMedium,
// Playback speed
OverlayButton(label = if (playbackSpeed == 1f) "1×" else "${playbackSpeed}×") {
showSpeedDialog = true
}
// Audio-only toggle
OverlayButton(label = if (audioOnly) "📻" else "📺") {
audioOnly = !audioOnly
// Disable / enable video renderer via track-selection params.
exoPlayer.trackSelectionParameters = TrackSelectionParameters.Builder(context)
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, audioOnly)
.build()
Toast.makeText(
context,
if (audioOnly) "audio-only" else "video on",
Toast.LENGTH_SHORT,
).show()
}
// Share
OverlayButton(label = "") {
val send = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, streamUrl)
putExtra(Intent.EXTRA_SUBJECT, title)
}
context.startActivity(Intent.createChooser(send, "Share video"))
}
// PiP
OverlayButton(label = "") {
val activity = (context as? Activity) ?: return@OverlayButton
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
runCatching { activity.enterPictureInPictureMode(params) }
}
}
}
if (showSpeedDialog) {
SpeedPickerDialog(
current = playbackSpeed,
onPick = { s ->
playbackSpeed = s
exoPlayer.playbackParameters = PlaybackParameters(s)
showSpeedDialog = false
},
onDismiss = { showSpeedDialog = false },
)
}
}
@ -269,6 +315,56 @@ fun PlayerScreen(
}
}
@Composable
private fun OverlayButton(label: String, onClick: () -> Unit) {
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(6.dp))
.background(Color(0xCC222222))
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
Text(label, color = Color.White, style = MaterialTheme.typography.titleSmall)
}
}
@Composable
private fun SpeedPickerDialog(
current: Float,
onPick: (Float) -> Unit,
onDismiss: () -> Unit,
) {
val options = listOf(0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Playback speed") },
text = {
Column {
options.forEach { s ->
Row(
modifier = Modifier
.fillMaxSize()
.clickable { onPick(s) }
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = if (s == current) "${s}×" else " ${s}×",
style = MaterialTheme.typography.bodyLarge,
color = if (s == current) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface,
)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) { Text("Close") }
},
)
}
/**
* Returns the segment whose interval contains [posSec], if any, skipping
* UUIDs in [skipped]. Filters out POI-style point segments (start == end).