mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 13:38:13 -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 =
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ void main(List<String> args) async {
|
|||
applicationInfoProvider.overrideWith((ref) => applicationInfo),
|
||||
crashLogProvider.overrideWith((ref) => crashProvider),
|
||||
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args, leanBackEnabled)),
|
||||
syncProvider.overrideWith((ref) => SyncNotifier(ref, applicationDirectory))
|
||||
syncProvider.overrideWith((ref) => SyncNotifier(ref, applicationDirectory)),
|
||||
],
|
||||
child: AdaptiveLayoutBuilder(
|
||||
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/services.dart';
|
||||
|
||||
|
|
@ -8,13 +5,10 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.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/video_player_settings.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/src/player_settings_helper.g.dart' as pigeon;
|
||||
|
||||
final videoPlayerSettingsProvider =
|
||||
StateNotifierProvider<VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) {
|
||||
|
|
@ -33,34 +27,6 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
|||
final oldState = super.state;
|
||||
super.state = 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)) {
|
||||
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/home_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/video_player_settings_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
|
|
@ -25,6 +26,9 @@ final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
|||
|
||||
final sharedUtilityProvider = Provider<SharedUtility>((ref) {
|
||||
final sharedPrefs = ref.watch(sharedPreferencesProvider);
|
||||
|
||||
//Init pigeon settings sync for native
|
||||
ref.read(pigeonPlayerSettingsSyncProvider);
|
||||
return SharedUtility(ref: ref, sharedPreferences: sharedPrefs);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,15 +24,12 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
|||
items: ThemeMode.values,
|
||||
selected: [ref.read(clientSettingsProvider.select((value) => value.themeMode))],
|
||||
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeMode(values.first),
|
||||
itemBuilder: (type, selected, tap) => RadioGroup(
|
||||
groupValue: ref.read(clientSettingsProvider.select((value) => value.themeMode)),
|
||||
itemBuilder: (type, selected, tap) => CheckboxListTile(
|
||||
value: selected,
|
||||
onChanged: (value) => tap(),
|
||||
child: RadioListTile(
|
||||
value: type,
|
||||
title: Text(type.label(context)),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
title: Text(type.label(context)),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -41,43 +38,40 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
|||
subLabel: Text(clientSettings.themeColor?.name ?? context.localized.dynamicText),
|
||||
onTap: () => openMultiSelectOptions<ColorThemes?>(
|
||||
context,
|
||||
label: context.localized.settingsLayoutModesTitle,
|
||||
label: context.localized.color,
|
||||
items: [null, ...ColorThemes.values],
|
||||
selected: [(ref.read(clientSettingsProvider.select((value) => value.themeColor)))],
|
||||
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeColor(values.first),
|
||||
itemBuilder: (type, selected, tap) => RadioGroup(
|
||||
itemBuilder: (type, selected, tap) => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: selected,
|
||||
onChanged: (value) => tap(),
|
||||
groupValue: ref.read(clientSettingsProvider.select((value) => value.themeColor)),
|
||||
child: RadioListTile<ColorThemes?>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: type,
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 24,
|
||||
width: 24,
|
||||
decoration: BoxDecoration(
|
||||
gradient: type == null
|
||||
? const SweepGradient(
|
||||
center: FractionalOffset.center,
|
||||
colors: <Color>[
|
||||
Color(0xFF4285F4), // blue
|
||||
Color(0xFF34A853), // green
|
||||
Color(0xFFFBBC05), // yellow
|
||||
Color(0xFFEA4335), // red
|
||||
Color(0xFF4285F4), // blue again to seamlessly transition to the start
|
||||
],
|
||||
stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0],
|
||||
)
|
||||
: null,
|
||||
color: type?.color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 24,
|
||||
width: 24,
|
||||
decoration: BoxDecoration(
|
||||
gradient: type == null
|
||||
? const SweepGradient(
|
||||
center: FractionalOffset.center,
|
||||
colors: <Color>[
|
||||
Color(0xFF4285F4), // blue
|
||||
Color(0xFF34A853), // green
|
||||
Color(0xFFFBBC05), // yellow
|
||||
Color(0xFFEA4335), // red
|
||||
Color(0xFF4285F4), // blue again to seamlessly transition to the start
|
||||
],
|
||||
stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0],
|
||||
)
|
||||
: null,
|
||||
color: type?.color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(type?.name ?? context.localized.dynamicText),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(type?.name ?? context.localized.dynamicText),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -88,18 +82,15 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
|
|||
onTap: () async {
|
||||
await openMultiSelectOptions<DynamicSchemeVariant>(
|
||||
context,
|
||||
label: context.localized.settingsLayoutModesTitle,
|
||||
label: context.localized.clientSettingsSchemeVariantTitle,
|
||||
items: DynamicSchemeVariant.values,
|
||||
selected: [(ref.read(clientSettingsProvider.select((value) => value.schemeVariant)))],
|
||||
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setSchemeVariant(values.first),
|
||||
itemBuilder: (type, selected, tap) => RadioGroup(
|
||||
itemBuilder: (type, selected, tap) => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: selected,
|
||||
onChanged: (value) => tap(),
|
||||
groupValue: selected ? type : null,
|
||||
child: RadioListTile<DynamicSchemeVariant>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: type,
|
||||
title: Text(type.label(context)),
|
||||
),
|
||||
title: Text(type.label(context)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class PlayerSettings {
|
|||
PlayerSettings({
|
||||
required this.enableTunneling,
|
||||
required this.skipTypes,
|
||||
this.themeColor,
|
||||
required this.skipForward,
|
||||
required this.skipBackward,
|
||||
});
|
||||
|
|
@ -55,6 +56,8 @@ class PlayerSettings {
|
|||
|
||||
Map<SegmentType, SegmentSkip> skipTypes;
|
||||
|
||||
int? themeColor;
|
||||
|
||||
int skipForward;
|
||||
|
||||
int skipBackward;
|
||||
|
|
@ -63,6 +66,7 @@ class PlayerSettings {
|
|||
return <Object?>[
|
||||
enableTunneling,
|
||||
skipTypes,
|
||||
themeColor,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
];
|
||||
|
|
@ -76,8 +80,9 @@ class PlayerSettings {
|
|||
return PlayerSettings(
|
||||
enableTunneling: result[0]! as bool,
|
||||
skipTypes: (result[1] as Map<Object?, Object?>?)!.cast<SegmentType, SegmentSkip>(),
|
||||
skipForward: result[2]! as int,
|
||||
skipBackward: result[3]! as int,
|
||||
themeColor: result[2] 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(
|
||||
title: Text(label),
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
width: MediaQuery.of(context).size.width * 0.85,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@ import 'package:pigeon/pigeon.dart';
|
|||
class PlayerSettings {
|
||||
final bool enableTunneling;
|
||||
final Map<SegmentType, SegmentSkip> skipTypes;
|
||||
//Color in ARGB32 format
|
||||
final int? themeColor;
|
||||
final int skipForward;
|
||||
final int skipBackward;
|
||||
|
||||
const PlayerSettings({
|
||||
required this.enableTunneling,
|
||||
required this.skipTypes,
|
||||
required this.themeColor,
|
||||
required this.skipForward,
|
||||
required this.skipBackward,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue