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.
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
25
strawApp/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
5
strawApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
BIN
strawApp/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 987 B |
BIN
strawApp/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 987 B |
BIN
strawApp/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
strawApp/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
strawApp/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
strawApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
strawApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
strawApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
strawApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
strawApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |