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:
parent
7894fe5a4d
commit
253c5e268b
3 changed files with 137 additions and 27 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue