diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index baf5b07e4..cdfaaf1e4 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -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" 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 53438ce80..1d0d40155 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 @@ -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) diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt index 01dc37a56..6ae8fd696 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt @@ -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).