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
// strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no
// NewPipeExtractor in the runtime path.
const val STRAW_VERSION_CODE = 27
const val STRAW_VERSION_NAME = "0.1.0-AM"
const val STRAW_VERSION_CODE = 28
const val STRAW_VERSION_NAME = "0.1.0-AN"
const val STRAW_APPLICATION_ID = "com.sulkta.straw"

View file

@ -14,8 +14,8 @@
<application
android:name=".StrawApp"
android:label="@string/app_name"
android:icon="@android:drawable/sym_def_app_icon"
android:roundIcon="@android:drawable/sym_def_app_icon"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@ -118,6 +121,16 @@ fun VideoDetailScreen(
var inlinePlaying by remember(streamUrl) { mutableStateOf(false) }
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
// (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
@ -127,6 +140,25 @@ fun VideoDetailScreen(
val dismissThresholdPx = with(density) { 140.dp.toPx() }
val dragY = remember { Animatable(0f) }
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(
modifier = Modifier
@ -141,49 +173,27 @@ fun VideoDetailScreen(
scaleY = s
}
.statusBarsPadding()
.verticalScroll(rememberScrollState())
.padding(16.dp),
.verticalScroll(rememberScrollState()),
) {
when {
state.loading -> Box(
modifier = Modifier.fillMaxWidth().padding(top = 64.dp),
modifier = Modifier
.fillMaxWidth()
.padding(top = 64.dp),
contentAlignment = Alignment.Center,
) { CircularProgressIndicator() }
state.error != null -> Text(
"error: ${state.error}",
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp),
)
else -> {
val d = state.detail ?: return@Column
// Drag-to-minimize gesture lives on the player surface
// itself — same pattern YouTube/NewPipe use. Outside the
// 16:9 box the page scrolls normally, so the drag never
// 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),
)
}
},
)
}
// Player surface — edge-to-edge, NewPipe/YouTube style.
// Lives outside the 16dp horizontal padding so the
// thumbnail fills the screen width with no gutters.
if (inlinePlaying) {
InlinePlayer(
streamUrl = streamUrl,
@ -194,7 +204,6 @@ fun VideoDetailScreen(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black)
.then(playerDragModifier),
)
@ -203,7 +212,7 @@ fun VideoDetailScreen(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black)
.clickable { inlinePlaying = true }
.then(playerDragModifier),
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 = d.title,
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