vc=28: edge-to-edge player, nav-bar inset, video-track reset, app icon

Three layout/playback fixes on vc=26-27 feedback plus the long-deferred
app icon.

Layout — VideoDetailScreen
  Player surface now fills the screen width (no 16dp side gutters).
  Outer Column dropped its 16dp padding; the player Box hangs off the
  full width with no rounded corners — NewPipe/YouTube look. Everything
  below (title, chips, button row, description, related list) goes back
  into an inner Column with 16dp horizontal + 12dp vertical padding so
  the body still reads correctly.
  Bottom inset: Spacer(windowInsetsBottomHeight(WindowInsets.navigationBars))
  appended at the end of the scrollable column. Last related video can
  scroll up past the gesture pill / 3-button nav instead of being
  obscured by it. (Plain navigationBarsPadding would have pushed the
  whole surface up and left a dead band.)

Black-video fix
  vc=27's Background button disabled the video track on the controller
  via setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) and that override
  is sticky. Returning to a video left it audio-only with a black
  surface. Added a LaunchedEffect(controller, streamUrl) that resets
  TrackSelectionParameters to defaults on every entry into detail — if
  the user opened a video page, they want video. The audio-only
  fullscreen toggle and the Background button still set the override
  for the duration of that session; they just no longer leak.

App icon
  Replaced the Android default placeholder (sym_def_app_icon, which
  fdroid was failing to render anyway) with a proper adaptive icon:
    Background: #166534 deep green (sulkta.com brand)
    Foreground: tilted lime parallelogram + white play triangle
      (literal "straw" nod + video-app affordance)
  Adaptive XML in mipmap-anydpi-v26/. PNG fallbacks rendered via
  rsvg-convert at all five mipmap densities (mdpi/hdpi/xhdpi/xxhdpi/
  xxxhdpi) for pre-API-26 devices. Manifest now points at
  @mipmap/ic_launcher and @mipmap/ic_launcher_round.
This commit is contained in:
Kayos 2026-05-25 11:43:38 -07:00
parent 35f5affec3
commit 2e339814fd
17 changed files with 94 additions and 38 deletions

View file

@ -55,6 +55,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app"
// vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via
// strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no // strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no
// NewPipeExtractor in the runtime path. // NewPipeExtractor in the runtime path.
const val STRAW_VERSION_CODE = 27 const val STRAW_VERSION_CODE = 28
const val STRAW_VERSION_NAME = "0.1.0-AM" const val STRAW_VERSION_NAME = "0.1.0-AN"
const val STRAW_APPLICATION_ID = "com.sulkta.straw" const val STRAW_APPLICATION_ID = "com.sulkta.straw"

View file

@ -14,8 +14,8 @@
<application <application
android:name=".StrawApp" android:name=".StrawApp"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@android:drawable/sym_def_app_icon" android:icon="@mipmap/ic_launcher"
android:roundIcon="@android:drawable/sym_def_app_icon" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.Material.Light.NoActionBar"> android:theme="@android:style/Theme.Material.Light.NoActionBar">

View file

@ -25,14 +25,17 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
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.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -118,6 +121,16 @@ fun VideoDetailScreen(
var inlinePlaying by remember(streamUrl) { mutableStateOf(false) } var inlinePlaying by remember(streamUrl) { mutableStateOf(false) }
LaunchedEffect(streamUrl) { vm.load(streamUrl) } LaunchedEffect(streamUrl) { vm.load(streamUrl) }
// The Background button (and the fullscreen audio-only toggle)
// disable the video track on the shared controller, and that state
// sticks. Entering detail = user wants to watch the video — wipe the
// override and let DASH pick the highest renderable video again.
LaunchedEffect(controller, streamUrl) {
controller?.let {
it.trackSelectionParameters = TrackSelectionParameters.Builder(context).build()
}
}
// Swipe-down to minimize. The drag handle is the inline player surface // Swipe-down to minimize. The drag handle is the inline player surface
// (the 16:9 box at the top); we translate the WHOLE page with it so the // (the 16:9 box at the top); we translate the WHOLE page with it so the
// motion reads as "the video is being tucked away" rather than "this // motion reads as "the video is being tucked away" rather than "this
@ -127,6 +140,25 @@ fun VideoDetailScreen(
val dismissThresholdPx = with(density) { 140.dp.toPx() } val dismissThresholdPx = with(density) { 140.dp.toPx() }
val dragY = remember { Animatable(0f) } val dragY = remember { Animatable(0f) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val playerDragModifier = Modifier.pointerInput(Unit) {
detectVerticalDragGestures(
onDragEnd = {
if (dragY.value > dismissThresholdPx) {
onMinimize()
} else {
scope.launch { dragY.animateTo(0f, spring()) }
}
},
onDragCancel = {
scope.launch { dragY.animateTo(0f, spring()) }
},
onVerticalDrag = { _, dy ->
scope.launch {
dragY.snapTo((dragY.value + dy).coerceAtLeast(0f))
}
},
)
}
Column( Column(
modifier = Modifier modifier = Modifier
@ -141,49 +173,27 @@ fun VideoDetailScreen(
scaleY = s scaleY = s
} }
.statusBarsPadding() .statusBarsPadding()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState()),
.padding(16.dp),
) { ) {
when { when {
state.loading -> Box( state.loading -> Box(
modifier = Modifier.fillMaxWidth().padding(top = 64.dp), modifier = Modifier
.fillMaxWidth()
.padding(top = 64.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { CircularProgressIndicator() } ) { CircularProgressIndicator() }
state.error != null -> Text( state.error != null -> Text(
"error: ${state.error}", "error: ${state.error}",
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp),
) )
else -> { else -> {
val d = state.detail ?: return@Column val d = state.detail ?: return@Column
// Drag-to-minimize gesture lives on the player surface // Player surface — edge-to-edge, NewPipe/YouTube style.
// itself — same pattern YouTube/NewPipe use. Outside the // Lives outside the 16dp horizontal padding so the
// 16:9 box the page scrolls normally, so the drag never // thumbnail fills the screen width with no gutters.
// fights with description scrolling.
val playerDragModifier = Modifier.pointerInput(Unit) {
detectVerticalDragGestures(
onDragEnd = {
if (dragY.value > dismissThresholdPx) {
onMinimize()
} else {
scope.launch {
dragY.animateTo(0f, spring())
}
}
},
onDragCancel = {
scope.launch { dragY.animateTo(0f, spring()) }
},
onVerticalDrag = { _, dy ->
scope.launch {
dragY.snapTo(
(dragY.value + dy).coerceAtLeast(0f),
)
}
},
)
}
if (inlinePlaying) { if (inlinePlaying) {
InlinePlayer( InlinePlayer(
streamUrl = streamUrl, streamUrl = streamUrl,
@ -194,7 +204,6 @@ fun VideoDetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(16f / 9f) .aspectRatio(16f / 9f)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black) .background(Color.Black)
.then(playerDragModifier), .then(playerDragModifier),
) )
@ -203,7 +212,7 @@ fun VideoDetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(16f / 9f) .aspectRatio(16f / 9f)
.clip(RoundedCornerShape(8.dp)) .background(Color.Black)
.clickable { inlinePlaying = true } .clickable { inlinePlaying = true }
.then(playerDragModifier), .then(playerDragModifier),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@ -229,8 +238,9 @@ fun VideoDetailScreen(
} }
} }
} }
Spacer(modifier = Modifier.height(12.dp)) // Everything below the player gets the side gutters
// back; player itself remains edge-to-edge.
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
Text( Text(
text = d.title, text = d.title,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -492,8 +502,14 @@ fun VideoDetailScreen(
}, },
) )
} }
} // close inner Column (padded body)
} }
} }
// Leave room at the bottom for the system nav bar so the last
// related video doesn't tuck under the gesture pill / 3-button
// nav. Compose's `navigationBarsPadding` would push the whole
// surface up; we want the scroll to extend past it instead.
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
} }
} }

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#166534" />
</shape>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Adaptive icon foreground for Straw. Canvas is 108x108dp; the visible
mask-safe area is roughly the central 66x66dp (centered at 54,54).
Two shapes:
- a tilted "straw" rectangle (lime, 4ADE80) running diagonally,
a literal nod to the app name
- a clean white play triangle sitting on top, anchoring the meaning
as a video player
Foreground sits on the deep-green (#166534) ic_launcher_background.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Tilted straw shape — parallelogram, lime green, slight rotation. -->
<path
android:fillColor="#4ADE80"
android:pathData="M 62,18 L 76,22 L 50,90 L 36,86 Z" />
<!-- White play triangle, centered. -->
<path
android:fillColor="#FFFFFF"
android:pathData="M 44,40 L 78,57 L 44,74 Z" />
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB