mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: Bunch of small UI improvements for native player
This commit is contained in:
parent
66ffc8c112
commit
edbd8d467c
23 changed files with 329 additions and 327 deletions
|
|
@ -1,30 +1,45 @@
|
|||
package nl.jknaapen.fladder
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.materialkolor.PaletteStyle
|
||||
import com.materialkolor.dynamiccolor.ColorSpec
|
||||
import com.materialkolor.rememberDynamicColorScheme
|
||||
import nl.jknaapen.fladder.objects.PlayerSettingsObject
|
||||
|
||||
@Composable
|
||||
fun VideoPlayerTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = rememberDynamicColorScheme(
|
||||
seedColor = Color(0xFFFF9800),
|
||||
val context = LocalContext.current
|
||||
|
||||
val themeColor by PlayerSettingsObject.themeColor.collectAsState(null)
|
||||
|
||||
val generatedScheme = rememberDynamicColorScheme(
|
||||
seedColor = themeColor ?: Color(0xFFFF9800),
|
||||
isDark = true,
|
||||
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||
style = PaletteStyle.Expressive,
|
||||
)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
) {
|
||||
CompositionLocalProvider {
|
||||
content()
|
||||
|
||||
val chosenScheme: ColorScheme =
|
||||
if (themeColor == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
dynamicLightColorScheme(context)
|
||||
} else {
|
||||
generatedScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = chosenScheme,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ fun VideoPlayerScreen(
|
|||
) {
|
||||
val leanBackEnabled = leanBackEnabled(LocalContext.current)
|
||||
ExoPlayer { player ->
|
||||
ScaledContent(if (leanBackEnabled) 0.50f else 1f) {
|
||||
ScaledContent(if (leanBackEnabled) 0.75f else 1f) {
|
||||
CustomVideoControls(player)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ enum class SegmentSkip(val raw: Int) {
|
|||
data class PlayerSettings (
|
||||
val enableTunneling: Boolean,
|
||||
val skipTypes: Map<SegmentType, SegmentSkip>,
|
||||
val themeColor: Long? = null,
|
||||
val skipForward: Long,
|
||||
val skipBackward: Long
|
||||
)
|
||||
|
|
@ -103,15 +104,17 @@ data class PlayerSettings (
|
|||
fun fromList(pigeonVar_list: List<Any?>): PlayerSettings {
|
||||
val enableTunneling = pigeonVar_list[0] as Boolean
|
||||
val skipTypes = pigeonVar_list[1] as Map<SegmentType, SegmentSkip>
|
||||
val skipForward = pigeonVar_list[2] as Long
|
||||
val skipBackward = pigeonVar_list[3] as Long
|
||||
return PlayerSettings(enableTunneling, skipTypes, skipForward, skipBackward)
|
||||
val themeColor = pigeonVar_list[2] as Long?
|
||||
val skipForward = pigeonVar_list[3] as Long
|
||||
val skipBackward = pigeonVar_list[4] as Long
|
||||
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
enableTunneling,
|
||||
skipTypes,
|
||||
themeColor,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -57,7 +56,6 @@ internal fun CustomIconButton(
|
|||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.wrapContentSize() // parent expands to fit children
|
||||
.conditional(enableScaledFocus) {
|
||||
scale(if (isFocused) 1.05f else 1f)
|
||||
}
|
||||
|
|
@ -69,11 +67,11 @@ internal fun CustomIconButton(
|
|||
indication = null,
|
||||
onClick = onClick
|
||||
)
|
||||
.alpha(if (enabled) 1f else 0.5f),
|
||||
.alpha(if (enabled) 1f else 0.15f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentColor provides currentContentColor) {
|
||||
Box(modifier = Modifier.padding(16.dp)) {
|
||||
Box(modifier = Modifier.padding(8.dp)) {
|
||||
icon()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -23,6 +24,7 @@ fun ItemHeader(state: PlayableData?) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ internal fun ProgressBar(
|
|||
val position by VideoPlayerObject.position.collectAsState(0L)
|
||||
val duration by VideoPlayerObject.duration.collectAsState(0L)
|
||||
|
||||
val endTimeString by VideoPlayerObject.endTime.collectAsState(null)
|
||||
|
||||
var tempPosition by remember { mutableLongStateOf(position) }
|
||||
var scrubbingTimeLine by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ internal fun ProgressBar(
|
|||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.CenterVertically)
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp, alignment = Alignment.CenterVertically)
|
||||
) {
|
||||
val playbackData by VideoPlayerObject.implementation.playbackData.collectAsState(null)
|
||||
if (scrubbingTimeLine)
|
||||
|
|
@ -115,21 +117,25 @@ internal fun ProgressBar(
|
|||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val subTitle = playableData?.subTitle
|
||||
subTitle?.let {
|
||||
val progressBarTopLabel = listOf(
|
||||
playableData?.subTitle,
|
||||
endTimeString,
|
||||
)
|
||||
|
||||
val label = progressBarTopLabel.joinToString(separator = " - ")
|
||||
if (label.isNotBlank()) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
)
|
||||
}
|
||||
VideoEndTime()
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
16.dp,
|
||||
12.dp,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -138,7 +144,7 @@ internal fun ProgressBar(
|
|||
Text(
|
||||
formatTime(currentPosition),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
|
|
@ -163,7 +169,7 @@ internal fun ProgressBar(
|
|||
)
|
||||
),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
|
|
@ -212,7 +218,7 @@ internal fun RowScope.SimpleProgressBar(
|
|||
width = it.size.width
|
||||
}
|
||||
)
|
||||
.heightIn(min = 42.dp)
|
||||
.heightIn(min = 32.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { offset ->
|
||||
onUserInteraction()
|
||||
|
|
@ -253,7 +259,7 @@ internal fun RowScope.SimpleProgressBar(
|
|||
modifier = Modifier
|
||||
.focusable(enabled = false)
|
||||
.fillMaxWidth()
|
||||
.height(10.dp)
|
||||
.height(9.dp)
|
||||
.background(
|
||||
color = Color.White.copy(
|
||||
alpha = 0.15f
|
||||
|
|
@ -307,10 +313,10 @@ internal fun RowScope.SimpleProgressBar(
|
|||
.focusable(enabled = false)
|
||||
.graphicsLayer {
|
||||
translationX = startPx
|
||||
translationY = 20.dp.toPx()
|
||||
translationY = 13.dp.toPx()
|
||||
}
|
||||
.width(segDp)
|
||||
.height(8.dp)
|
||||
.height(7.dp)
|
||||
.background(
|
||||
color = segment.color.copy(alpha = 0.75f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import androidx.compose.foundation.border
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -20,22 +19,17 @@ import androidx.compose.foundation.layout.safeContentPadding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -61,8 +55,6 @@ internal fun BoxScope.SegmentSkipOverlay(
|
|||
val player = VideoPlayerObject.implementation.player
|
||||
val skipMap by PlayerSettingsObject.skipMap.collectAsState(mapOf())
|
||||
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(segments, skipMap) { }
|
||||
|
||||
if (segments.isEmpty() || player == null) return
|
||||
|
|
@ -91,7 +83,7 @@ internal fun BoxScope.SegmentSkipOverlay(
|
|||
}
|
||||
}
|
||||
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
RoundedCornerShape(8.dp)
|
||||
|
||||
AnimatedVisibility(
|
||||
activeSegment != null && skip == SegmentSkip.ASK,
|
||||
|
|
@ -100,69 +92,53 @@ internal fun BoxScope.SegmentSkipOverlay(
|
|||
.padding(16.dp)
|
||||
.safeContentPadding()
|
||||
) {
|
||||
Box {
|
||||
FilledTonalButton(
|
||||
modifier = modifier
|
||||
.align(alignment = Alignment.CenterEnd)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { state ->
|
||||
isFocused = state.isFocused
|
||||
}
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = if (isFocused) Color.White.copy(alpha = 0.4f) else Color.Transparent,
|
||||
shape = shape,
|
||||
)
|
||||
.defaultSelected(true),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
shape = shape,
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = Color.White.copy(alpha = 0.75f),
|
||||
contentColor = Color.Black,
|
||||
),
|
||||
onClick = {
|
||||
activeSegment?.let {
|
||||
player.seekTo(it.end)
|
||||
}
|
||||
CustomIconButton(
|
||||
modifier = modifier
|
||||
.align(alignment = Alignment.CenterEnd)
|
||||
.focusRequester(focusRequester)
|
||||
.defaultSelected(true),
|
||||
onClick = {
|
||||
activeSegment?.let {
|
||||
player.seekTo(it.end)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (isAndroidTV) {
|
||||
if (isAndroidTV) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.15f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.border(
|
||||
width = 1.5.dp,
|
||||
color = Color.Black.copy(alpha = 0.15f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.padding(7.dp)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.15f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.border(
|
||||
width = 1.5.dp,
|
||||
color = Color.Black.copy(alpha = 0.15f),
|
||||
color = Color.White,
|
||||
shape = CircleShape,
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(7.dp)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = CircleShape,
|
||||
)
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
activeSegment?.let {
|
||||
Text(
|
||||
"Skip ${it.name.lowercase()}",
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
}
|
||||
}
|
||||
activeSegment?.let {
|
||||
Text(
|
||||
"Skip ${it.name.lowercase()}",
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package nl.jknaapen.fladder.composables.controls
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
fun VideoEndTime() {
|
||||
val startInstant = remember { Clock.System.now() }
|
||||
val durationMs by VideoPlayerObject.duration.collectAsState(initial = 0L)
|
||||
val zone = ZoneId.systemDefault()
|
||||
|
||||
val javaInstant = remember(startInstant) { startInstant.toJavaInstant() }
|
||||
val endJavaInstant = remember(javaInstant, durationMs) {
|
||||
javaInstant.plusMillis(durationMs)
|
||||
}
|
||||
val endZoned = remember(endJavaInstant, zone) {
|
||||
endJavaInstant.atZone(zone)
|
||||
}
|
||||
|
||||
val formatter = DateTimeFormatter.ofPattern("hh:mm a")
|
||||
val formattedEnd = remember(endZoned, formatter) {
|
||||
endZoned.format(formatter)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "ends at $formattedEnd",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
|
|||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -97,24 +98,31 @@ fun CustomVideoControls(
|
|||
|
||||
val buffering by VideoPlayerObject.buffering.collectAsState(true)
|
||||
val playing by VideoPlayerObject.playing.collectAsState(false)
|
||||
val controlsPadding = 32.dp
|
||||
val controlsPadding = 16.dp
|
||||
|
||||
ImmersiveSystemBars(isImmersive = !showControls)
|
||||
|
||||
val bottomControlFocusRequester = remember { FocusRequester() }
|
||||
|
||||
fun hideControls() {
|
||||
showControls = false
|
||||
bottomControlFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
|
||||
BackHandler(
|
||||
enabled = showControls
|
||||
) {
|
||||
showControls = false
|
||||
hideControls()
|
||||
}
|
||||
|
||||
|
||||
// Restart the hide timer whenever `lastInteraction` changes.
|
||||
LaunchedEffect(lastInteraction.longValue) {
|
||||
delay(5.seconds)
|
||||
showControls = false
|
||||
hideControls()
|
||||
}
|
||||
|
||||
val bottomControlFocusRequester = remember { FocusRequester() }
|
||||
|
||||
fun updateLastInteraction() {
|
||||
showControls = true
|
||||
lastInteraction.longValue = System.currentTimeMillis()
|
||||
|
|
@ -167,7 +175,7 @@ fun CustomVideoControls(
|
|||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0.85f),
|
||||
Color.Black.copy(alpha = 0.90f),
|
||||
Color.Black.copy(alpha = 0f),
|
||||
),
|
||||
start = Offset(0f, 0f),
|
||||
|
|
@ -175,10 +183,11 @@ fun CustomVideoControls(
|
|||
),
|
||||
)
|
||||
.safeContentPadding(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.Start)
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.Start)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
val state by VideoPlayerObject.implementation.playbackData.collectAsState(
|
||||
null
|
||||
|
|
@ -196,7 +205,7 @@ fun CustomVideoControls(
|
|||
Icon(
|
||||
Iconsax.Outline.CloseSquare,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.size(38.dp)
|
||||
.focusable(false),
|
||||
contentDescription = "Close icon",
|
||||
tint = Color.White,
|
||||
|
|
@ -209,40 +218,39 @@ fun CustomVideoControls(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = controlsPadding)
|
||||
.displayCutoutPadding(),
|
||||
) {
|
||||
ProgressBar(
|
||||
modifier = Modifier,
|
||||
exoPlayer, bottomControlFocusRequester, ::updateLastInteraction
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0f),
|
||||
Color.Black.copy(alpha = 0.85f),
|
||||
Color.Black.copy(alpha = 0.9f),
|
||||
),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(0f, Float.POSITIVE_INFINITY)
|
||||
),
|
||||
)
|
||||
.displayCutoutPadding()
|
||||
.padding(horizontal = controlsPadding)
|
||||
.padding(top = 8.dp, bottom = controlsPadding)
|
||||
) {
|
||||
LeftButtons(
|
||||
openChapterSelection = {
|
||||
showChapterDialog = true
|
||||
}
|
||||
ProgressBar(
|
||||
modifier = Modifier,
|
||||
exoPlayer, bottomControlFocusRequester, ::updateLastInteraction
|
||||
)
|
||||
PlaybackButtons(exoPlayer, bottomControlFocusRequester)
|
||||
RightButtons(showAudioDialog, showSubDialog)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
LeftButtons(
|
||||
openChapterSelection = {
|
||||
showChapterDialog = true
|
||||
}
|
||||
)
|
||||
PlaybackButtons(exoPlayer, bottomControlFocusRequester)
|
||||
RightButtons(showAudioDialog, showSubDialog)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
SegmentSkipOverlay()
|
||||
|
|
@ -252,7 +260,7 @@ fun CustomVideoControls(
|
|||
.align(alignment = Alignment.Center)
|
||||
.size(64.dp),
|
||||
strokeCap = StrokeCap.Round,
|
||||
strokeWidth = 12.dp
|
||||
strokeWidth = 10.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -310,7 +318,7 @@ fun PlaybackButtons(
|
|||
.padding(horizontal = 4.dp, vertical = 6.dp)
|
||||
.wrapContentWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
16.dp,
|
||||
12.dp,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -321,7 +329,6 @@ fun PlaybackButtons(
|
|||
) {
|
||||
Icon(
|
||||
Iconsax.Filled.Backward,
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentDescription = previousVideo,
|
||||
)
|
||||
}
|
||||
|
|
@ -339,11 +346,13 @@ fun PlaybackButtons(
|
|||
) {
|
||||
Icon(
|
||||
Iconsax.Outline.Refresh,
|
||||
modifier = Modifier.size(38.dp),
|
||||
contentDescription = "Forward",
|
||||
modifier = Modifier
|
||||
.size(48.dp),
|
||||
)
|
||||
Text("-${backwardSpeed.inWholeSeconds}")
|
||||
Text(
|
||||
"-${backwardSpeed.inWholeSeconds}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
CustomIconButton(
|
||||
|
|
@ -357,7 +366,7 @@ fun PlaybackButtons(
|
|||
) {
|
||||
Icon(
|
||||
if (isPlaying) Iconsax.Filled.Pause else Iconsax.Filled.Play,
|
||||
modifier = Modifier.size(55.dp),
|
||||
modifier = Modifier.size(42.dp),
|
||||
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||
)
|
||||
}
|
||||
|
|
@ -377,10 +386,13 @@ fun PlaybackButtons(
|
|||
Iconsax.Outline.Refresh,
|
||||
contentDescription = "Forward",
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.size(38.dp)
|
||||
.scale(scaleX = -1f, scaleY = 1f),
|
||||
)
|
||||
Text(forwardSpeed.inWholeSeconds.toString())
|
||||
Text(
|
||||
forwardSpeed.inWholeSeconds.toString(),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -390,7 +402,6 @@ fun PlaybackButtons(
|
|||
) {
|
||||
Icon(
|
||||
Iconsax.Filled.Forward,
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentDescription = nextVideo,
|
||||
)
|
||||
}
|
||||
|
|
@ -405,7 +416,7 @@ internal fun RowScope.LeftButtons(
|
|||
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.Start)
|
||||
) {
|
||||
CustomIconButton(
|
||||
onClick = openChapterSelection,
|
||||
|
|
@ -413,7 +424,6 @@ internal fun RowScope.LeftButtons(
|
|||
) {
|
||||
Icon(
|
||||
Iconsax.Filled.Check,
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentDescription = "Show chapters",
|
||||
)
|
||||
}
|
||||
|
|
@ -427,7 +437,7 @@ internal fun RowScope.RightButtons(
|
|||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.End
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.End)
|
||||
) {
|
||||
CustomIconButton(
|
||||
onClick = {
|
||||
|
|
@ -436,7 +446,6 @@ internal fun RowScope.RightButtons(
|
|||
) {
|
||||
Icon(
|
||||
Iconsax.Filled.AudioSquare,
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentDescription = "Audio Track",
|
||||
)
|
||||
}
|
||||
|
|
@ -447,7 +456,6 @@ internal fun RowScope.RightButtons(
|
|||
) {
|
||||
Icon(
|
||||
Iconsax.Filled.Subtitle,
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentDescription = "Subtitles",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package nl.jknaapen.fladder.composables.dialogs
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -48,7 +47,6 @@ fun AudioPicker(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
item {
|
||||
val selectedOff = -1 == selectedIndex
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
|
|
@ -31,6 +32,10 @@ internal fun CustomModalBottomSheet(
|
|||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
modalBottomSheetState.expand()
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest,
|
||||
dragHandle = null,
|
||||
|
|
@ -43,13 +48,13 @@ internal fun CustomModalBottomSheet(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.systemBarsPadding()
|
||||
.displayCutoutPadding()
|
||||
.background(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0f),
|
||||
Color.Black.copy(alpha = 0.8f),
|
||||
Color.Black.copy(alpha = 0.65f),
|
||||
Color.Black.copy(alpha = 0.85f),
|
||||
),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(0f, Float.POSITIVE_INFINITY)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package nl.jknaapen.fladder.composables.dialogs
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -12,7 +11,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
|
@ -48,9 +46,8 @@ fun SubtitlePicker(
|
|||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
item {
|
||||
val selectedOff = -1 == selectedIndex
|
||||
|
|
@ -64,17 +61,9 @@ fun SubtitlePicker(
|
|||
},
|
||||
selected = selectedOff
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
alignment = Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Off",
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Off",
|
||||
)
|
||||
}
|
||||
}
|
||||
internalSubTracks.forEachIndexed { index, subtitle ->
|
||||
|
|
@ -93,17 +82,9 @@ fun SubtitlePicker(
|
|||
},
|
||||
selected = selected,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
alignment = Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = serverSub?.name ?: "",
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = serverSub?.name ?: "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
package nl.jknaapen.fladder.composables.dialogs
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.rabehx.iconsax.Iconsax
|
||||
import io.github.rabehx.iconsax.filled.TickSquare
|
||||
import nl.jknaapen.fladder.composables.controls.CustomIconButton
|
||||
import nl.jknaapen.fladder.utility.defaultSelected
|
||||
|
||||
@Composable
|
||||
internal fun TrackButton(
|
||||
|
|
@ -23,30 +26,30 @@ internal fun TrackButton(
|
|||
selected: Boolean = false,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val backgroundColor = if (selected) Color.White else Color.Black
|
||||
val textColor = if (selected) Color.Black else Color.White
|
||||
val textStyle =
|
||||
MaterialTheme.typography.bodyLarge.copy(color = textColor, fontWeight = FontWeight.Bold)
|
||||
MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
|
||||
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
TextButton(
|
||||
CustomIconButton(
|
||||
backgroundColor = Color.White.copy(alpha = 0.25f),
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = backgroundColor.copy(alpha = 0.65f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
.padding(12.dp)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
),
|
||||
.padding(vertical = 6.dp, horizontal = 12.dp)
|
||||
.defaultMinSize(minHeight = 40.dp)
|
||||
.defaultSelected(selected),
|
||||
onClick = onClick,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
CompositionLocalProvider(LocalTextStyle provides textStyle) {
|
||||
content()
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Iconsax.Filled.TickSquare,
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(LocalTextStyle provides textStyle) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package nl.jknaapen.fladder.objects
|
|||
|
||||
import PlayerSettings
|
||||
import PlayerSettingsPigeon
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.time.DurationUnit
|
||||
|
|
@ -21,6 +22,12 @@ object PlayerSettingsObject : PlayerSettingsPigeon {
|
|||
)
|
||||
}
|
||||
|
||||
val themeColor = settings.map { settings ->
|
||||
settings?.themeColor.let {
|
||||
if (it == null) null else Color(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendPlayerSettings(playerSettings: PlayerSettings) {
|
||||
settings.value = playerSettings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,20 @@ package nl.jknaapen.fladder.objects
|
|||
import PlaybackState
|
||||
import VideoPlayerControlsCallback
|
||||
import VideoPlayerListenerCallback
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import nl.jknaapen.fladder.VideoPlayerActivity
|
||||
import nl.jknaapen.fladder.messengers.VideoPlayerImplementation
|
||||
import nl.jknaapen.fladder.utility.InternalTrack
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
object VideoPlayerObject {
|
||||
val implementation: VideoPlayerImplementation = VideoPlayerImplementation()
|
||||
|
|
@ -23,6 +31,20 @@ object VideoPlayerObject {
|
|||
|
||||
val chapters = implementation.playbackData.map { it?.chapters }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalTime::class)
|
||||
val endTime = combine(position, duration) { pos, dur ->
|
||||
val startInstant = Clock.System.now()
|
||||
val zone = ZoneId.systemDefault()
|
||||
|
||||
val remainingMs = (dur - pos).coerceAtLeast(0L)
|
||||
val endInstant = startInstant.toJavaInstant().plusMillis(remainingMs)
|
||||
val endZoned = endInstant.atZone(zone)
|
||||
val formatter = DateTimeFormatter.ofPattern("hh:mm a")
|
||||
|
||||
"ends at ${endZoned.format(formatter)}"
|
||||
}
|
||||
|
||||
val currentSubtitleTrackIndex =
|
||||
MutableStateFlow((implementation.playbackData.value?.defaultSubtrack ?: -1).toInt())
|
||||
val currentAudioTrackIndex =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue