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
|
// 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"
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |