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 a tester's "where are all the features"): quality picker, related videos on detail, download (audio + video).
This commit is contained in:
parent
3a7c596244
commit
7c31ced8e3
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"
|
const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app"
|
||||||
|
|
||||||
// Sulkta fork — Straw
|
// Sulkta fork — Straw
|
||||||
const val STRAW_VERSION_CODE = 1
|
const val STRAW_VERSION_CODE = 2
|
||||||
const val STRAW_VERSION_NAME = "0.1.0-day1"
|
const val STRAW_VERSION_NAME = "0.1.0-N"
|
||||||
const val STRAW_APPLICATION_ID = "com.sulkta.straw"
|
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.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -56,6 +59,7 @@ fun VideoDetailScreen(
|
||||||
vm: VideoDetailViewModel = viewModel(),
|
vm: VideoDetailViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val state by vm.ui.collectAsStateWithLifecycle()
|
val state by vm.ui.collectAsStateWithLifecycle()
|
||||||
|
val context = LocalContext.current
|
||||||
LaunchedEffect(streamUrl) { vm.load(streamUrl) }
|
LaunchedEffect(streamUrl) { vm.load(streamUrl) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -160,7 +164,17 @@ fun VideoDetailScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
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.Activity
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
import android.widget.Toast
|
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
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.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
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.draw.clip
|
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.AudioAttributes
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.PlaybackParameters
|
||||||
import androidx.media3.common.Player
|
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.common.util.UnstableApi
|
||||||
import androidx.media3.datasource.DefaultHttpDataSource
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
|
@ -70,6 +84,11 @@ fun PlayerScreen(
|
||||||
val state by vm.ui.collectAsStateWithLifecycle()
|
val state by vm.ui.collectAsStateWithLifecycle()
|
||||||
LaunchedEffect(streamUrl) { vm.resolve(streamUrl) }
|
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 {
|
val exoPlayer = remember {
|
||||||
ExoPlayer.Builder(context)
|
ExoPlayer.Builder(context)
|
||||||
.setAudioAttributes(
|
.setAudioAttributes(
|
||||||
|
|
@ -237,31 +256,58 @@ fun PlayerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// PiP button — top-right. Tapping it puts the player into
|
// Top-right overlay — speed / audio-only / share / PiP.
|
||||||
// floating-window mode so the user can use other apps while
|
Row(
|
||||||
// the video keeps playing.
|
modifier = Modifier.align(Alignment.TopEnd).padding(12.dp),
|
||||||
Box(
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
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,
|
|
||||||
) {
|
) {
|
||||||
Text(
|
// Playback speed
|
||||||
text = "⊟",
|
OverlayButton(label = if (playbackSpeed == 1f) "1×" else "${playbackSpeed}×") {
|
||||||
color = Color.White,
|
showSpeedDialog = true
|
||||||
style = MaterialTheme.typography.titleMedium,
|
}
|
||||||
|
// 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
|
* Returns the segment whose interval contains [posSec], if any, skipping
|
||||||
* UUIDs in [skipped]. Filters out POI-style point segments (start == end).
|
* UUIDs in [skipped]. Filters out POI-style point segments (start == end).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue