mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07: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
|
package nl.jknaapen.fladder
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.materialkolor.PaletteStyle
|
import com.materialkolor.PaletteStyle
|
||||||
import com.materialkolor.dynamiccolor.ColorSpec
|
import com.materialkolor.dynamiccolor.ColorSpec
|
||||||
import com.materialkolor.rememberDynamicColorScheme
|
import com.materialkolor.rememberDynamicColorScheme
|
||||||
|
import nl.jknaapen.fladder.objects.PlayerSettingsObject
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerTheme(
|
fun VideoPlayerTheme(
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = rememberDynamicColorScheme(
|
val context = LocalContext.current
|
||||||
seedColor = Color(0xFFFF9800),
|
|
||||||
|
val themeColor by PlayerSettingsObject.themeColor.collectAsState(null)
|
||||||
|
|
||||||
|
val generatedScheme = rememberDynamicColorScheme(
|
||||||
|
seedColor = themeColor ?: Color(0xFFFF9800),
|
||||||
isDark = true,
|
isDark = true,
|
||||||
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||||
style = PaletteStyle.Expressive,
|
style = PaletteStyle.Expressive,
|
||||||
)
|
)
|
||||||
|
|
||||||
MaterialTheme(
|
val chosenScheme: ColorScheme =
|
||||||
colorScheme = colorScheme,
|
if (themeColor == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
) {
|
dynamicLightColorScheme(context)
|
||||||
CompositionLocalProvider {
|
} else {
|
||||||
content()
|
generatedScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = chosenScheme,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ fun VideoPlayerScreen(
|
||||||
) {
|
) {
|
||||||
val leanBackEnabled = leanBackEnabled(LocalContext.current)
|
val leanBackEnabled = leanBackEnabled(LocalContext.current)
|
||||||
ExoPlayer { player ->
|
ExoPlayer { player ->
|
||||||
ScaledContent(if (leanBackEnabled) 0.50f else 1f) {
|
ScaledContent(if (leanBackEnabled) 0.75f else 1f) {
|
||||||
CustomVideoControls(player)
|
CustomVideoControls(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ enum class SegmentSkip(val raw: Int) {
|
||||||
data class PlayerSettings (
|
data class PlayerSettings (
|
||||||
val enableTunneling: Boolean,
|
val enableTunneling: Boolean,
|
||||||
val skipTypes: Map<SegmentType, SegmentSkip>,
|
val skipTypes: Map<SegmentType, SegmentSkip>,
|
||||||
|
val themeColor: Long? = null,
|
||||||
val skipForward: Long,
|
val skipForward: Long,
|
||||||
val skipBackward: Long
|
val skipBackward: Long
|
||||||
)
|
)
|
||||||
|
|
@ -103,15 +104,17 @@ data class PlayerSettings (
|
||||||
fun fromList(pigeonVar_list: List<Any?>): PlayerSettings {
|
fun fromList(pigeonVar_list: List<Any?>): PlayerSettings {
|
||||||
val enableTunneling = pigeonVar_list[0] as Boolean
|
val enableTunneling = pigeonVar_list[0] as Boolean
|
||||||
val skipTypes = pigeonVar_list[1] as Map<SegmentType, SegmentSkip>
|
val skipTypes = pigeonVar_list[1] as Map<SegmentType, SegmentSkip>
|
||||||
val skipForward = pigeonVar_list[2] as Long
|
val themeColor = pigeonVar_list[2] as Long?
|
||||||
val skipBackward = pigeonVar_list[3] as Long
|
val skipForward = pigeonVar_list[3] as Long
|
||||||
return PlayerSettings(enableTunneling, skipTypes, skipForward, skipBackward)
|
val skipBackward = pigeonVar_list[4] as Long
|
||||||
|
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
return listOf(
|
return listOf(
|
||||||
enableTunneling,
|
enableTunneling,
|
||||||
skipTypes,
|
skipTypes,
|
||||||
|
themeColor,
|
||||||
skipForward,
|
skipForward,
|
||||||
skipBackward,
|
skipBackward,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -57,7 +56,6 @@ internal fun CustomIconButton(
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.wrapContentSize() // parent expands to fit children
|
|
||||||
.conditional(enableScaledFocus) {
|
.conditional(enableScaledFocus) {
|
||||||
scale(if (isFocused) 1.05f else 1f)
|
scale(if (isFocused) 1.05f else 1f)
|
||||||
}
|
}
|
||||||
|
|
@ -69,11 +67,11 @@ internal fun CustomIconButton(
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
)
|
)
|
||||||
.alpha(if (enabled) 1f else 0.5f),
|
.alpha(if (enabled) 1f else 0.15f),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalContentColor provides currentContentColor) {
|
CompositionLocalProvider(LocalContentColor provides currentContentColor) {
|
||||||
Box(modifier = Modifier.padding(16.dp)) {
|
Box(modifier = Modifier.padding(8.dp)) {
|
||||||
icon()
|
icon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -23,6 +24,7 @@ fun ItemHeader(state: PlayableData?) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ internal fun ProgressBar(
|
||||||
val position by VideoPlayerObject.position.collectAsState(0L)
|
val position by VideoPlayerObject.position.collectAsState(0L)
|
||||||
val duration by VideoPlayerObject.duration.collectAsState(0L)
|
val duration by VideoPlayerObject.duration.collectAsState(0L)
|
||||||
|
|
||||||
|
val endTimeString by VideoPlayerObject.endTime.collectAsState(null)
|
||||||
|
|
||||||
var tempPosition by remember { mutableLongStateOf(position) }
|
var tempPosition by remember { mutableLongStateOf(position) }
|
||||||
var scrubbingTimeLine by remember { mutableStateOf(false) }
|
var scrubbingTimeLine by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -99,7 +101,7 @@ internal fun ProgressBar(
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
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)
|
val playbackData by VideoPlayerObject.implementation.playbackData.collectAsState(null)
|
||||||
if (scrubbingTimeLine)
|
if (scrubbingTimeLine)
|
||||||
|
|
@ -115,21 +117,25 @@ internal fun ProgressBar(
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
val subTitle = playableData?.subTitle
|
val progressBarTopLabel = listOf(
|
||||||
subTitle?.let {
|
playableData?.subTitle,
|
||||||
|
endTimeString,
|
||||||
|
)
|
||||||
|
|
||||||
|
val label = progressBarTopLabel.joinToString(separator = " - ")
|
||||||
|
if (label.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = label,
|
||||||
style = MaterialTheme.typography.titleLarge.copy(
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
VideoEndTime()
|
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
16.dp,
|
12.dp,
|
||||||
alignment = Alignment.CenterHorizontally
|
alignment = Alignment.CenterHorizontally
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -138,7 +144,7 @@ internal fun ProgressBar(
|
||||||
Text(
|
Text(
|
||||||
formatTime(currentPosition),
|
formatTime(currentPosition),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = MaterialTheme.typography.titleLarge.copy(
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -163,7 +169,7 @@ internal fun ProgressBar(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = MaterialTheme.typography.titleLarge.copy(
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -212,7 +218,7 @@ internal fun RowScope.SimpleProgressBar(
|
||||||
width = it.size.width
|
width = it.size.width
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.heightIn(min = 42.dp)
|
.heightIn(min = 32.dp)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures { offset ->
|
detectTapGestures { offset ->
|
||||||
onUserInteraction()
|
onUserInteraction()
|
||||||
|
|
@ -253,7 +259,7 @@ internal fun RowScope.SimpleProgressBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusable(enabled = false)
|
.focusable(enabled = false)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(10.dp)
|
.height(9.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color.White.copy(
|
color = Color.White.copy(
|
||||||
alpha = 0.15f
|
alpha = 0.15f
|
||||||
|
|
@ -307,10 +313,10 @@ internal fun RowScope.SimpleProgressBar(
|
||||||
.focusable(enabled = false)
|
.focusable(enabled = false)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
translationX = startPx
|
translationX = startPx
|
||||||
translationY = 20.dp.toPx()
|
translationY = 13.dp.toPx()
|
||||||
}
|
}
|
||||||
.width(segDp)
|
.width(segDp)
|
||||||
.height(8.dp)
|
.height(7.dp)
|
||||||
.background(
|
.background(
|
||||||
color = segment.color.copy(alpha = 0.75f),
|
color = segment.color.copy(alpha = 0.75f),
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
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.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -61,8 +55,6 @@ internal fun BoxScope.SegmentSkipOverlay(
|
||||||
val player = VideoPlayerObject.implementation.player
|
val player = VideoPlayerObject.implementation.player
|
||||||
val skipMap by PlayerSettingsObject.skipMap.collectAsState(mapOf())
|
val skipMap by PlayerSettingsObject.skipMap.collectAsState(mapOf())
|
||||||
|
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(segments, skipMap) { }
|
LaunchedEffect(segments, skipMap) { }
|
||||||
|
|
||||||
if (segments.isEmpty() || player == null) return
|
if (segments.isEmpty() || player == null) return
|
||||||
|
|
@ -91,7 +83,7 @@ internal fun BoxScope.SegmentSkipOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val shape = RoundedCornerShape(8.dp)
|
RoundedCornerShape(8.dp)
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
activeSegment != null && skip == SegmentSkip.ASK,
|
activeSegment != null && skip == SegmentSkip.ASK,
|
||||||
|
|
@ -100,26 +92,11 @@ internal fun BoxScope.SegmentSkipOverlay(
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.safeContentPadding()
|
.safeContentPadding()
|
||||||
) {
|
) {
|
||||||
Box {
|
CustomIconButton(
|
||||||
FilledTonalButton(
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.align(alignment = Alignment.CenterEnd)
|
.align(alignment = Alignment.CenterEnd)
|
||||||
.focusRequester(focusRequester)
|
.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),
|
.defaultSelected(true),
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
|
||||||
shape = shape,
|
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = Color.White.copy(alpha = 0.75f),
|
|
||||||
contentColor = Color.Black,
|
|
||||||
),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
activeSegment?.let {
|
activeSegment?.let {
|
||||||
player.seekTo(it.end)
|
player.seekTo(it.end)
|
||||||
|
|
@ -166,7 +143,6 @@ internal fun BoxScope.SegmentSkipOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val MediaSegmentType.toSegment: SegmentType
|
private val MediaSegmentType.toSegment: SegmentType
|
||||||
|
|
|
||||||
|
|
@ -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.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -97,24 +98,31 @@ fun CustomVideoControls(
|
||||||
|
|
||||||
val buffering by VideoPlayerObject.buffering.collectAsState(true)
|
val buffering by VideoPlayerObject.buffering.collectAsState(true)
|
||||||
val playing by VideoPlayerObject.playing.collectAsState(false)
|
val playing by VideoPlayerObject.playing.collectAsState(false)
|
||||||
val controlsPadding = 32.dp
|
val controlsPadding = 16.dp
|
||||||
|
|
||||||
ImmersiveSystemBars(isImmersive = !showControls)
|
ImmersiveSystemBars(isImmersive = !showControls)
|
||||||
|
|
||||||
|
val bottomControlFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
fun hideControls() {
|
||||||
|
showControls = false
|
||||||
|
bottomControlFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
BackHandler(
|
BackHandler(
|
||||||
enabled = showControls
|
enabled = showControls
|
||||||
) {
|
) {
|
||||||
showControls = false
|
hideControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Restart the hide timer whenever `lastInteraction` changes.
|
// Restart the hide timer whenever `lastInteraction` changes.
|
||||||
LaunchedEffect(lastInteraction.longValue) {
|
LaunchedEffect(lastInteraction.longValue) {
|
||||||
delay(5.seconds)
|
delay(5.seconds)
|
||||||
showControls = false
|
hideControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
val bottomControlFocusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
fun updateLastInteraction() {
|
fun updateLastInteraction() {
|
||||||
showControls = true
|
showControls = true
|
||||||
lastInteraction.longValue = System.currentTimeMillis()
|
lastInteraction.longValue = System.currentTimeMillis()
|
||||||
|
|
@ -167,7 +175,7 @@ fun CustomVideoControls(
|
||||||
.background(
|
.background(
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.Black.copy(alpha = 0.85f),
|
Color.Black.copy(alpha = 0.90f),
|
||||||
Color.Black.copy(alpha = 0f),
|
Color.Black.copy(alpha = 0f),
|
||||||
),
|
),
|
||||||
start = Offset(0f, 0f),
|
start = Offset(0f, 0f),
|
||||||
|
|
@ -175,10 +183,11 @@ fun CustomVideoControls(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.safeContentPadding(),
|
.safeContentPadding(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.Start)
|
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.Start)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
val state by VideoPlayerObject.implementation.playbackData.collectAsState(
|
val state by VideoPlayerObject.implementation.playbackData.collectAsState(
|
||||||
null
|
null
|
||||||
|
|
@ -196,7 +205,7 @@ fun CustomVideoControls(
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Outline.CloseSquare,
|
Iconsax.Outline.CloseSquare,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(38.dp)
|
||||||
.focusable(false),
|
.focusable(false),
|
||||||
contentDescription = "Close icon",
|
contentDescription = "Close icon",
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
|
|
@ -209,31 +218,28 @@ fun CustomVideoControls(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = controlsPadding)
|
.padding(horizontal = controlsPadding)
|
||||||
.displayCutoutPadding(),
|
|
||||||
) {
|
|
||||||
ProgressBar(
|
|
||||||
modifier = Modifier,
|
|
||||||
exoPlayer, bottomControlFocusRequester, ::updateLastInteraction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
.background(
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.Black.copy(alpha = 0f),
|
Color.Black.copy(alpha = 0f),
|
||||||
Color.Black.copy(alpha = 0.85f),
|
Color.Black.copy(alpha = 0.9f),
|
||||||
),
|
),
|
||||||
start = Offset(0f, 0f),
|
start = Offset(0f, 0f),
|
||||||
end = Offset(0f, Float.POSITIVE_INFINITY)
|
end = Offset(0f, Float.POSITIVE_INFINITY)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.displayCutoutPadding()
|
.displayCutoutPadding()
|
||||||
.padding(horizontal = controlsPadding)
|
|
||||||
.padding(top = 8.dp, bottom = controlsPadding)
|
.padding(top = 8.dp, bottom = controlsPadding)
|
||||||
|
) {
|
||||||
|
ProgressBar(
|
||||||
|
modifier = Modifier,
|
||||||
|
exoPlayer, bottomControlFocusRequester, ::updateLastInteraction
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
LeftButtons(
|
LeftButtons(
|
||||||
openChapterSelection = {
|
openChapterSelection = {
|
||||||
|
|
@ -244,6 +250,8 @@ fun CustomVideoControls(
|
||||||
RightButtons(showAudioDialog, showSubDialog)
|
RightButtons(showAudioDialog, showSubDialog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SegmentSkipOverlay()
|
SegmentSkipOverlay()
|
||||||
if (buffering && !playing) {
|
if (buffering && !playing) {
|
||||||
|
|
@ -252,7 +260,7 @@ fun CustomVideoControls(
|
||||||
.align(alignment = Alignment.Center)
|
.align(alignment = Alignment.Center)
|
||||||
.size(64.dp),
|
.size(64.dp),
|
||||||
strokeCap = StrokeCap.Round,
|
strokeCap = StrokeCap.Round,
|
||||||
strokeWidth = 12.dp
|
strokeWidth = 10.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +318,7 @@ fun PlaybackButtons(
|
||||||
.padding(horizontal = 4.dp, vertical = 6.dp)
|
.padding(horizontal = 4.dp, vertical = 6.dp)
|
||||||
.wrapContentWidth(),
|
.wrapContentWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
16.dp,
|
12.dp,
|
||||||
alignment = Alignment.CenterHorizontally
|
alignment = Alignment.CenterHorizontally
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -321,7 +329,6 @@ fun PlaybackButtons(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Filled.Backward,
|
Iconsax.Filled.Backward,
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
contentDescription = previousVideo,
|
contentDescription = previousVideo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -339,11 +346,13 @@ fun PlaybackButtons(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Outline.Refresh,
|
Iconsax.Outline.Refresh,
|
||||||
|
modifier = Modifier.size(38.dp),
|
||||||
contentDescription = "Forward",
|
contentDescription = "Forward",
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp),
|
|
||||||
)
|
)
|
||||||
Text("-${backwardSpeed.inWholeSeconds}")
|
Text(
|
||||||
|
"-${backwardSpeed.inWholeSeconds}",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CustomIconButton(
|
CustomIconButton(
|
||||||
|
|
@ -357,7 +366,7 @@ fun PlaybackButtons(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
if (isPlaying) Iconsax.Filled.Pause else Iconsax.Filled.Play,
|
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",
|
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -377,10 +386,13 @@ fun PlaybackButtons(
|
||||||
Iconsax.Outline.Refresh,
|
Iconsax.Outline.Refresh,
|
||||||
contentDescription = "Forward",
|
contentDescription = "Forward",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(38.dp)
|
||||||
.scale(scaleX = -1f, scaleY = 1f),
|
.scale(scaleX = -1f, scaleY = 1f),
|
||||||
)
|
)
|
||||||
Text(forwardSpeed.inWholeSeconds.toString())
|
Text(
|
||||||
|
forwardSpeed.inWholeSeconds.toString(),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,7 +402,6 @@ fun PlaybackButtons(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Filled.Forward,
|
Iconsax.Filled.Forward,
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
contentDescription = nextVideo,
|
contentDescription = nextVideo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +416,7 @@ internal fun RowScope.LeftButtons(
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
horizontalArrangement = Arrangement.Start
|
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.Start)
|
||||||
) {
|
) {
|
||||||
CustomIconButton(
|
CustomIconButton(
|
||||||
onClick = openChapterSelection,
|
onClick = openChapterSelection,
|
||||||
|
|
@ -413,7 +424,6 @@ internal fun RowScope.LeftButtons(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Filled.Check,
|
Iconsax.Filled.Check,
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
contentDescription = "Show chapters",
|
contentDescription = "Show chapters",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -427,7 +437,7 @@ internal fun RowScope.RightButtons(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.End)
|
||||||
) {
|
) {
|
||||||
CustomIconButton(
|
CustomIconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -436,7 +446,6 @@ internal fun RowScope.RightButtons(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Filled.AudioSquare,
|
Iconsax.Filled.AudioSquare,
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
contentDescription = "Audio Track",
|
contentDescription = "Audio Track",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -447,7 +456,6 @@ internal fun RowScope.RightButtons(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Filled.Subtitle,
|
Iconsax.Filled.Subtitle,
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
contentDescription = "Subtitles",
|
contentDescription = "Subtitles",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package nl.jknaapen.fladder.composables.dialogs
|
package nl.jknaapen.fladder.composables.dialogs
|
||||||
|
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -48,7 +47,6 @@ fun AudioPicker(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 16.dp),
|
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
val selectedOff = -1 == selectedIndex
|
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.displayCutoutPadding
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
|
@ -31,6 +32,10 @@ internal fun CustomModalBottomSheet(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
modalBottomSheetState.expand()
|
||||||
|
}
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest,
|
onDismissRequest,
|
||||||
dragHandle = null,
|
dragHandle = null,
|
||||||
|
|
@ -43,13 +48,13 @@ internal fun CustomModalBottomSheet(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.systemBarsPadding()
|
|
||||||
.displayCutoutPadding()
|
.displayCutoutPadding()
|
||||||
.background(
|
.background(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.Black.copy(alpha = 0f),
|
Color.Black.copy(alpha = 0.65f),
|
||||||
Color.Black.copy(alpha = 0.8f),
|
Color.Black.copy(alpha = 0.85f),
|
||||||
),
|
),
|
||||||
start = Offset(0f, 0f),
|
start = Offset(0f, 0f),
|
||||||
end = Offset(0f, Float.POSITIVE_INFINITY)
|
end = Offset(0f, Float.POSITIVE_INFINITY)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package nl.jknaapen.fladder.composables.dialogs
|
package nl.jknaapen.fladder.composables.dialogs
|
||||||
|
|
||||||
import androidx.annotation.OptIn
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -12,7 +11,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
|
@ -48,9 +46,8 @@ fun SubtitlePicker(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.wrapContentWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 16.dp),
|
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
val selectedOff = -1 == selectedIndex
|
val selectedOff = -1 == selectedIndex
|
||||||
|
|
@ -63,20 +60,12 @@ fun SubtitlePicker(
|
||||||
player.clearSubtitleTrack()
|
player.clearSubtitleTrack()
|
||||||
},
|
},
|
||||||
selected = selectedOff
|
selected = selectedOff
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
|
||||||
8.dp,
|
|
||||||
alignment = Alignment.CenterVertically
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Off",
|
text = "Off",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
internalSubTracks.forEachIndexed { index, subtitle ->
|
internalSubTracks.forEachIndexed { index, subtitle ->
|
||||||
val serverSub = subTitles.elementAtOrNull(index + 1)
|
val serverSub = subTitles.elementAtOrNull(index + 1)
|
||||||
val selected = serverSub?.index == selectedIndex.toLong()
|
val selected = serverSub?.index == selectedIndex.toLong()
|
||||||
|
|
@ -92,13 +81,6 @@ fun SubtitlePicker(
|
||||||
player.setInternalSubtitleTrack(subtitle)
|
player.setInternalSubtitleTrack(subtitle)
|
||||||
},
|
},
|
||||||
selected = selected,
|
selected = selected,
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
|
||||||
8.dp,
|
|
||||||
alignment = Alignment.CenterVertically
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = serverSub?.name ?: "",
|
text = serverSub?.name ?: "",
|
||||||
|
|
@ -108,5 +90,4 @@ fun SubtitlePicker(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
package nl.jknaapen.fladder.composables.dialogs
|
package nl.jknaapen.fladder.composables.dialogs
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.padding
|
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.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
@Composable
|
||||||
internal fun TrackButton(
|
internal fun TrackButton(
|
||||||
|
|
@ -23,30 +26,30 @@ internal fun TrackButton(
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (selected) Color.White else Color.Black
|
|
||||||
val textColor = if (selected) Color.Black else Color.White
|
|
||||||
val textStyle =
|
val textStyle =
|
||||||
MaterialTheme.typography.bodyLarge.copy(color = textColor, fontWeight = FontWeight.Bold)
|
MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
CustomIconButton(
|
||||||
|
backgroundColor = Color.White.copy(alpha = 0.25f),
|
||||||
TextButton(
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(
|
.padding(vertical = 6.dp, horizontal = 12.dp)
|
||||||
color = backgroundColor.copy(alpha = 0.65f),
|
.defaultMinSize(minHeight = 40.dp)
|
||||||
shape = RoundedCornerShape(12.dp),
|
.defaultSelected(selected),
|
||||||
)
|
|
||||||
.padding(12.dp)
|
|
||||||
.clickable(
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
),
|
|
||||||
onClick = onClick,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (selected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Iconsax.Filled.TickSquare,
|
||||||
|
contentDescription = "",
|
||||||
|
)
|
||||||
|
}
|
||||||
CompositionLocalProvider(LocalTextStyle provides textStyle) {
|
CompositionLocalProvider(LocalTextStyle provides textStyle) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package nl.jknaapen.fladder.objects
|
||||||
|
|
||||||
import PlayerSettings
|
import PlayerSettings
|
||||||
import PlayerSettingsPigeon
|
import PlayerSettingsPigeon
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlin.time.DurationUnit
|
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) {
|
override fun sendPlayerSettings(playerSettings: PlayerSettings) {
|
||||||
settings.value = playerSettings
|
settings.value = playerSettings
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,20 @@ package nl.jknaapen.fladder.objects
|
||||||
import PlaybackState
|
import PlaybackState
|
||||||
import VideoPlayerControlsCallback
|
import VideoPlayerControlsCallback
|
||||||
import VideoPlayerListenerCallback
|
import VideoPlayerListenerCallback
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import nl.jknaapen.fladder.VideoPlayerActivity
|
import nl.jknaapen.fladder.VideoPlayerActivity
|
||||||
import nl.jknaapen.fladder.messengers.VideoPlayerImplementation
|
import nl.jknaapen.fladder.messengers.VideoPlayerImplementation
|
||||||
import nl.jknaapen.fladder.utility.InternalTrack
|
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 {
|
object VideoPlayerObject {
|
||||||
val implementation: VideoPlayerImplementation = VideoPlayerImplementation()
|
val implementation: VideoPlayerImplementation = VideoPlayerImplementation()
|
||||||
|
|
@ -23,6 +31,20 @@ object VideoPlayerObject {
|
||||||
|
|
||||||
val chapters = implementation.playbackData.map { it?.chapters }
|
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 =
|
val currentSubtitleTrackIndex =
|
||||||
MutableStateFlow((implementation.playbackData.value?.defaultSubtrack ?: -1).toInt())
|
MutableStateFlow((implementation.playbackData.value?.defaultSubtrack ?: -1).toInt())
|
||||||
val currentAudioTrackIndex =
|
val currentAudioTrackIndex =
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ void main(List<String> args) async {
|
||||||
applicationInfoProvider.overrideWith((ref) => applicationInfo),
|
applicationInfoProvider.overrideWith((ref) => applicationInfo),
|
||||||
crashLogProvider.overrideWith((ref) => crashProvider),
|
crashLogProvider.overrideWith((ref) => crashProvider),
|
||||||
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args, leanBackEnabled)),
|
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args, leanBackEnabled)),
|
||||||
syncProvider.overrideWith((ref) => SyncNotifier(ref, applicationDirectory))
|
syncProvider.overrideWith((ref) => SyncNotifier(ref, applicationDirectory)),
|
||||||
],
|
],
|
||||||
child: AdaptiveLayoutBuilder(
|
child: AdaptiveLayoutBuilder(
|
||||||
child: (context) => const Main(),
|
child: (context) => const Main(),
|
||||||
|
|
|
||||||
57
lib/providers/settings/pigeon_player_settings_provider.dart
Normal file
57
lib/providers/settings/pigeon_player_settings_provider.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
|
import 'package:fladder/src/player_settings_helper.g.dart' as pigeon;
|
||||||
|
|
||||||
|
final pigeonPlayerSettingsSyncProvider = Provider<void>((ref) {
|
||||||
|
void sendSettings() {
|
||||||
|
final userData = ref.read(userProvider);
|
||||||
|
final color = ref.read(
|
||||||
|
clientSettingsProvider.select(
|
||||||
|
(value) => value.themeColor?.color.toARGB32(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final value = ref.read(videoPlayerSettingsProvider);
|
||||||
|
|
||||||
|
if (!kIsWeb && Platform.isAndroid) {
|
||||||
|
pigeon.PlayerSettingsPigeon().sendPlayerSettings(
|
||||||
|
pigeon.PlayerSettings(
|
||||||
|
enableTunneling: value.enableTunneling,
|
||||||
|
skipTypes: value.segmentSkipSettings.map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
|
switch (key) {
|
||||||
|
MediaSegmentType.unknown => pigeon.SegmentType.intro,
|
||||||
|
MediaSegmentType.commercial => pigeon.SegmentType.commercial,
|
||||||
|
MediaSegmentType.preview => pigeon.SegmentType.preview,
|
||||||
|
MediaSegmentType.recap => pigeon.SegmentType.recap,
|
||||||
|
MediaSegmentType.outro => pigeon.SegmentType.outro,
|
||||||
|
MediaSegmentType.intro => pigeon.SegmentType.intro,
|
||||||
|
},
|
||||||
|
switch (value) {
|
||||||
|
SegmentSkip.none => pigeon.SegmentSkip.none,
|
||||||
|
SegmentSkip.askToSkip => pigeon.SegmentSkip.ask,
|
||||||
|
SegmentSkip.skip => pigeon.SegmentSkip.skip,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
themeColor: color,
|
||||||
|
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
|
||||||
|
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.listen(userProvider, (_, __) => sendSettings());
|
||||||
|
ref.listen(clientSettingsProvider, (_, __) => sendSettings());
|
||||||
|
ref.listen(videoPlayerSettingsProvider, (_, __) => sendSettings());
|
||||||
|
|
||||||
|
sendSettings();
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
|
@ -8,13 +5,10 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
|
||||||
import 'package:fladder/models/settings/key_combinations.dart';
|
import 'package:fladder/models/settings/key_combinations.dart';
|
||||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
import 'package:fladder/providers/shared_provider.dart';
|
import 'package:fladder/providers/shared_provider.dart';
|
||||||
import 'package:fladder/providers/user_provider.dart';
|
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/src/player_settings_helper.g.dart' as pigeon;
|
|
||||||
|
|
||||||
final videoPlayerSettingsProvider =
|
final videoPlayerSettingsProvider =
|
||||||
StateNotifierProvider<VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) {
|
StateNotifierProvider<VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) {
|
||||||
|
|
@ -33,34 +27,6 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
||||||
final oldState = super.state;
|
final oldState = super.state;
|
||||||
super.state = value;
|
super.state = value;
|
||||||
ref.read(sharedUtilityProvider).videoPlayerSettings = value;
|
ref.read(sharedUtilityProvider).videoPlayerSettings = value;
|
||||||
if (!kIsWeb && Platform.isAndroid) {
|
|
||||||
final userData = ref.read(userProvider);
|
|
||||||
pigeon.PlayerSettingsPigeon().sendPlayerSettings(
|
|
||||||
pigeon.PlayerSettings(
|
|
||||||
enableTunneling: value.enableTunneling,
|
|
||||||
skipTypes: value.segmentSkipSettings.map(
|
|
||||||
(key, value) => MapEntry(
|
|
||||||
switch (key) {
|
|
||||||
MediaSegmentType.unknown => pigeon.SegmentType.intro,
|
|
||||||
MediaSegmentType.commercial => pigeon.SegmentType.commercial,
|
|
||||||
MediaSegmentType.preview => pigeon.SegmentType.preview,
|
|
||||||
MediaSegmentType.recap => pigeon.SegmentType.recap,
|
|
||||||
MediaSegmentType.outro => pigeon.SegmentType.outro,
|
|
||||||
MediaSegmentType.intro => pigeon.SegmentType.intro,
|
|
||||||
},
|
|
||||||
switch (value) {
|
|
||||||
SegmentSkip.none => pigeon.SegmentSkip.none,
|
|
||||||
SegmentSkip.askToSkip => pigeon.SegmentSkip.ask,
|
|
||||||
SegmentSkip.skip => pigeon.SegmentSkip.skip,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
|
|
||||||
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oldState.playerSame(value)) {
|
if (!oldState.playerSame(value)) {
|
||||||
ref.read(videoPlayerProvider.notifier).init();
|
ref.read(videoPlayerProvider.notifier).init();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/settings/pigeon_player_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/providers/user_provider.dart';
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
|
|
@ -25,6 +26,9 @@ final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
||||||
|
|
||||||
final sharedUtilityProvider = Provider<SharedUtility>((ref) {
|
final sharedUtilityProvider = Provider<SharedUtility>((ref) {
|
||||||
final sharedPrefs = ref.watch(sharedPreferencesProvider);
|
final sharedPrefs = ref.watch(sharedPreferencesProvider);
|
||||||
|
|
||||||
|
//Init pigeon settings sync for native
|
||||||
|
ref.read(pigeonPlayerSettingsSyncProvider);
|
||||||
return SharedUtility(ref: ref, sharedPreferences: sharedPrefs);
|
return SharedUtility(ref: ref, sharedPreferences: sharedPrefs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,33 +24,28 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
||||||
items: ThemeMode.values,
|
items: ThemeMode.values,
|
||||||
selected: [ref.read(clientSettingsProvider.select((value) => value.themeMode))],
|
selected: [ref.read(clientSettingsProvider.select((value) => value.themeMode))],
|
||||||
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeMode(values.first),
|
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeMode(values.first),
|
||||||
itemBuilder: (type, selected, tap) => RadioGroup(
|
itemBuilder: (type, selected, tap) => CheckboxListTile(
|
||||||
groupValue: ref.read(clientSettingsProvider.select((value) => value.themeMode)),
|
value: selected,
|
||||||
onChanged: (value) => tap(),
|
onChanged: (value) => tap(),
|
||||||
child: RadioListTile(
|
|
||||||
value: type,
|
|
||||||
title: Text(type.label(context)),
|
title: Text(type.label(context)),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.color),
|
label: Text(context.localized.color),
|
||||||
subLabel: Text(clientSettings.themeColor?.name ?? context.localized.dynamicText),
|
subLabel: Text(clientSettings.themeColor?.name ?? context.localized.dynamicText),
|
||||||
onTap: () => openMultiSelectOptions<ColorThemes?>(
|
onTap: () => openMultiSelectOptions<ColorThemes?>(
|
||||||
context,
|
context,
|
||||||
label: context.localized.settingsLayoutModesTitle,
|
label: context.localized.color,
|
||||||
items: [null, ...ColorThemes.values],
|
items: [null, ...ColorThemes.values],
|
||||||
selected: [(ref.read(clientSettingsProvider.select((value) => value.themeColor)))],
|
selected: [(ref.read(clientSettingsProvider.select((value) => value.themeColor)))],
|
||||||
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeColor(values.first),
|
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeColor(values.first),
|
||||||
itemBuilder: (type, selected, tap) => RadioGroup(
|
itemBuilder: (type, selected, tap) => CheckboxListTile(
|
||||||
onChanged: (value) => tap(),
|
|
||||||
groupValue: ref.read(clientSettingsProvider.select((value) => value.themeColor)),
|
|
||||||
child: RadioListTile<ColorThemes?>(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
value: type,
|
value: selected,
|
||||||
|
onChanged: (value) => tap(),
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
|
|
@ -81,26 +76,22 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.clientSettingsSchemeVariantTitle),
|
label: Text(context.localized.clientSettingsSchemeVariantTitle),
|
||||||
subLabel: Text(clientSettings.schemeVariant.label(context)),
|
subLabel: Text(clientSettings.schemeVariant.label(context)),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await openMultiSelectOptions<DynamicSchemeVariant>(
|
await openMultiSelectOptions<DynamicSchemeVariant>(
|
||||||
context,
|
context,
|
||||||
label: context.localized.settingsLayoutModesTitle,
|
label: context.localized.clientSettingsSchemeVariantTitle,
|
||||||
items: DynamicSchemeVariant.values,
|
items: DynamicSchemeVariant.values,
|
||||||
selected: [(ref.read(clientSettingsProvider.select((value) => value.schemeVariant)))],
|
selected: [(ref.read(clientSettingsProvider.select((value) => value.schemeVariant)))],
|
||||||
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setSchemeVariant(values.first),
|
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setSchemeVariant(values.first),
|
||||||
itemBuilder: (type, selected, tap) => RadioGroup(
|
itemBuilder: (type, selected, tap) => CheckboxListTile(
|
||||||
onChanged: (value) => tap(),
|
|
||||||
groupValue: selected ? type : null,
|
|
||||||
child: RadioListTile<DynamicSchemeVariant>(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
value: type,
|
value: selected,
|
||||||
|
onChanged: (value) => tap(),
|
||||||
title: Text(type.label(context)),
|
title: Text(type.label(context)),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ class PlayerSettings {
|
||||||
PlayerSettings({
|
PlayerSettings({
|
||||||
required this.enableTunneling,
|
required this.enableTunneling,
|
||||||
required this.skipTypes,
|
required this.skipTypes,
|
||||||
|
this.themeColor,
|
||||||
required this.skipForward,
|
required this.skipForward,
|
||||||
required this.skipBackward,
|
required this.skipBackward,
|
||||||
});
|
});
|
||||||
|
|
@ -55,6 +56,8 @@ class PlayerSettings {
|
||||||
|
|
||||||
Map<SegmentType, SegmentSkip> skipTypes;
|
Map<SegmentType, SegmentSkip> skipTypes;
|
||||||
|
|
||||||
|
int? themeColor;
|
||||||
|
|
||||||
int skipForward;
|
int skipForward;
|
||||||
|
|
||||||
int skipBackward;
|
int skipBackward;
|
||||||
|
|
@ -63,6 +66,7 @@ class PlayerSettings {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
enableTunneling,
|
enableTunneling,
|
||||||
skipTypes,
|
skipTypes,
|
||||||
|
themeColor,
|
||||||
skipForward,
|
skipForward,
|
||||||
skipBackward,
|
skipBackward,
|
||||||
];
|
];
|
||||||
|
|
@ -76,8 +80,9 @@ class PlayerSettings {
|
||||||
return PlayerSettings(
|
return PlayerSettings(
|
||||||
enableTunneling: result[0]! as bool,
|
enableTunneling: result[0]! as bool,
|
||||||
skipTypes: (result[1] as Map<Object?, Object?>?)!.cast<SegmentType, SegmentSkip>(),
|
skipTypes: (result[1] as Map<Object?, Object?>?)!.cast<SegmentType, SegmentSkip>(),
|
||||||
skipForward: result[2]! as int,
|
themeColor: result[2] as int?,
|
||||||
skipBackward: result[3]! as int,
|
skipForward: result[3]! as int,
|
||||||
|
skipBackward: result[4]! as int,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Future<List<T>> openMultiSelectOptions<T>(
|
||||||
builder: (context, setState) => AlertDialog(
|
builder: (context, setState) => AlertDialog(
|
||||||
title: Text(label),
|
title: Text(label),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.65,
|
width: MediaQuery.of(context).size.width * 0.85,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,15 @@ import 'package:pigeon/pigeon.dart';
|
||||||
class PlayerSettings {
|
class PlayerSettings {
|
||||||
final bool enableTunneling;
|
final bool enableTunneling;
|
||||||
final Map<SegmentType, SegmentSkip> skipTypes;
|
final Map<SegmentType, SegmentSkip> skipTypes;
|
||||||
|
//Color in ARGB32 format
|
||||||
|
final int? themeColor;
|
||||||
final int skipForward;
|
final int skipForward;
|
||||||
final int skipBackward;
|
final int skipBackward;
|
||||||
|
|
||||||
const PlayerSettings({
|
const PlayerSettings({
|
||||||
required this.enableTunneling,
|
required this.enableTunneling,
|
||||||
required this.skipTypes,
|
required this.skipTypes,
|
||||||
|
required this.themeColor,
|
||||||
required this.skipForward,
|
required this.skipForward,
|
||||||
required this.skipBackward,
|
required this.skipBackward,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue