mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
feat: Implement next-up screen for native player (#533)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
311b647286
commit
29b1c2e633
25 changed files with 782 additions and 203 deletions
|
|
@ -3,7 +3,7 @@ package nl.jknaapen.fladder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -31,7 +31,7 @@ fun VideoPlayerTheme(
|
||||||
|
|
||||||
val chosenScheme: ColorScheme =
|
val chosenScheme: ColorScheme =
|
||||||
if (themeColor == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (themeColor == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
dynamicLightColorScheme(context)
|
dynamicDarkColorScheme(context)
|
||||||
} else {
|
} else {
|
||||||
generatedScheme
|
generatedScheme
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,18 @@ private object PlayerSettingsHelperPigeonUtils {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class AutoNextType(val raw: Int) {
|
||||||
|
OFF(0),
|
||||||
|
STATIC(1),
|
||||||
|
SMART(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofRaw(raw: Int): AutoNextType? {
|
||||||
|
return values().firstOrNull { it.raw == raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class SegmentType(val raw: Int) {
|
enum class SegmentType(val raw: Int) {
|
||||||
COMMERCIAL(0),
|
COMMERCIAL(0),
|
||||||
PREVIEW(1),
|
PREVIEW(1),
|
||||||
|
|
@ -97,7 +109,8 @@ data class PlayerSettings (
|
||||||
val skipTypes: Map<SegmentType, SegmentSkip>,
|
val skipTypes: Map<SegmentType, SegmentSkip>,
|
||||||
val themeColor: Long? = null,
|
val themeColor: Long? = null,
|
||||||
val skipForward: Long,
|
val skipForward: Long,
|
||||||
val skipBackward: Long
|
val skipBackward: Long,
|
||||||
|
val autoNextType: AutoNextType
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -107,7 +120,8 @@ data class PlayerSettings (
|
||||||
val themeColor = pigeonVar_list[2] as Long?
|
val themeColor = pigeonVar_list[2] as Long?
|
||||||
val skipForward = pigeonVar_list[3] as Long
|
val skipForward = pigeonVar_list[3] as Long
|
||||||
val skipBackward = pigeonVar_list[4] as Long
|
val skipBackward = pigeonVar_list[4] as Long
|
||||||
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward)
|
val autoNextType = pigeonVar_list[5] as AutoNextType
|
||||||
|
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
|
|
@ -117,6 +131,7 @@ data class PlayerSettings (
|
||||||
themeColor,
|
themeColor,
|
||||||
skipForward,
|
skipForward,
|
||||||
skipBackward,
|
skipBackward,
|
||||||
|
autoNextType,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|
@ -135,15 +150,20 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
129.toByte() -> {
|
129.toByte() -> {
|
||||||
return (readValue(buffer) as Long?)?.let {
|
return (readValue(buffer) as Long?)?.let {
|
||||||
SegmentType.ofRaw(it.toInt())
|
AutoNextType.ofRaw(it.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
130.toByte() -> {
|
130.toByte() -> {
|
||||||
return (readValue(buffer) as Long?)?.let {
|
return (readValue(buffer) as Long?)?.let {
|
||||||
SegmentSkip.ofRaw(it.toInt())
|
SegmentType.ofRaw(it.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
131.toByte() -> {
|
131.toByte() -> {
|
||||||
|
return (readValue(buffer) as Long?)?.let {
|
||||||
|
SegmentSkip.ofRaw(it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
132.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
PlayerSettings.fromList(it)
|
PlayerSettings.fromList(it)
|
||||||
}
|
}
|
||||||
|
|
@ -153,16 +173,20 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
|
||||||
}
|
}
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
when (value) {
|
when (value) {
|
||||||
is SegmentType -> {
|
is AutoNextType -> {
|
||||||
stream.write(129)
|
stream.write(129)
|
||||||
writeValue(stream, value.raw)
|
writeValue(stream, value.raw)
|
||||||
}
|
}
|
||||||
is SegmentSkip -> {
|
is SegmentType -> {
|
||||||
stream.write(130)
|
stream.write(130)
|
||||||
writeValue(stream, value.raw)
|
writeValue(stream, value.raw)
|
||||||
}
|
}
|
||||||
is PlayerSettings -> {
|
is SegmentSkip -> {
|
||||||
stream.write(131)
|
stream.write(131)
|
||||||
|
writeValue(stream, value.raw)
|
||||||
|
}
|
||||||
|
is PlayerSettings -> {
|
||||||
|
stream.write(132)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
else -> super.writeValue(stream, value)
|
else -> super.writeValue(stream, value)
|
||||||
|
|
|
||||||
|
|
@ -95,11 +95,51 @@ enum class MediaSegmentType(val raw: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generated class from Pigeon that represents data sent in messages. */
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
data class PlayableData (
|
data class SimpleItemModel (
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val subTitle: String? = null,
|
val subTitle: String? = null,
|
||||||
|
val overview: String? = null,
|
||||||
val logoUrl: String? = null,
|
val logoUrl: String? = null,
|
||||||
|
val primaryPoster: String
|
||||||
|
)
|
||||||
|
{
|
||||||
|
companion object {
|
||||||
|
fun fromList(pigeonVar_list: List<Any?>): SimpleItemModel {
|
||||||
|
val id = pigeonVar_list[0] as String
|
||||||
|
val title = pigeonVar_list[1] as String
|
||||||
|
val subTitle = pigeonVar_list[2] as String?
|
||||||
|
val overview = pigeonVar_list[3] as String?
|
||||||
|
val logoUrl = pigeonVar_list[4] as String?
|
||||||
|
val primaryPoster = pigeonVar_list[5] as String
|
||||||
|
return SimpleItemModel(id, title, subTitle, overview, logoUrl, primaryPoster)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
overview,
|
||||||
|
logoUrl,
|
||||||
|
primaryPoster,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is SimpleItemModel) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this === other) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return VideoPlayerHelperPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||||
|
|
||||||
|
override fun hashCode(): Int = toList().hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class PlayableData (
|
||||||
|
val currentItem: SimpleItemModel,
|
||||||
val description: String,
|
val description: String,
|
||||||
val startPosition: Long,
|
val startPosition: Long,
|
||||||
val defaultAudioTrack: Long,
|
val defaultAudioTrack: Long,
|
||||||
|
|
@ -109,38 +149,32 @@ data class PlayableData (
|
||||||
val trickPlayModel: TrickPlayModel? = null,
|
val trickPlayModel: TrickPlayModel? = null,
|
||||||
val chapters: List<Chapter>,
|
val chapters: List<Chapter>,
|
||||||
val segments: List<MediaSegment>,
|
val segments: List<MediaSegment>,
|
||||||
val previousVideo: String? = null,
|
val previousVideo: SimpleItemModel? = null,
|
||||||
val nextVideo: String? = null,
|
val nextVideo: SimpleItemModel? = null,
|
||||||
val url: String
|
val url: String
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
fun fromList(pigeonVar_list: List<Any?>): PlayableData {
|
fun fromList(pigeonVar_list: List<Any?>): PlayableData {
|
||||||
val id = pigeonVar_list[0] as String
|
val currentItem = pigeonVar_list[0] as SimpleItemModel
|
||||||
val title = pigeonVar_list[1] as String
|
val description = pigeonVar_list[1] as String
|
||||||
val subTitle = pigeonVar_list[2] as String?
|
val startPosition = pigeonVar_list[2] as Long
|
||||||
val logoUrl = pigeonVar_list[3] as String?
|
val defaultAudioTrack = pigeonVar_list[3] as Long
|
||||||
val description = pigeonVar_list[4] as String
|
val audioTracks = pigeonVar_list[4] as List<AudioTrack>
|
||||||
val startPosition = pigeonVar_list[5] as Long
|
val defaultSubtrack = pigeonVar_list[5] as Long
|
||||||
val defaultAudioTrack = pigeonVar_list[6] as Long
|
val subtitleTracks = pigeonVar_list[6] as List<SubtitleTrack>
|
||||||
val audioTracks = pigeonVar_list[7] as List<AudioTrack>
|
val trickPlayModel = pigeonVar_list[7] as TrickPlayModel?
|
||||||
val defaultSubtrack = pigeonVar_list[8] as Long
|
val chapters = pigeonVar_list[8] as List<Chapter>
|
||||||
val subtitleTracks = pigeonVar_list[9] as List<SubtitleTrack>
|
val segments = pigeonVar_list[9] as List<MediaSegment>
|
||||||
val trickPlayModel = pigeonVar_list[10] as TrickPlayModel?
|
val previousVideo = pigeonVar_list[10] as SimpleItemModel?
|
||||||
val chapters = pigeonVar_list[11] as List<Chapter>
|
val nextVideo = pigeonVar_list[11] as SimpleItemModel?
|
||||||
val segments = pigeonVar_list[12] as List<MediaSegment>
|
val url = pigeonVar_list[12] as String
|
||||||
val previousVideo = pigeonVar_list[13] as String?
|
return PlayableData(currentItem, description, startPosition, defaultAudioTrack, audioTracks, defaultSubtrack, subtitleTracks, trickPlayModel, chapters, segments, previousVideo, nextVideo, url)
|
||||||
val nextVideo = pigeonVar_list[14] as String?
|
|
||||||
val url = pigeonVar_list[15] as String
|
|
||||||
return PlayableData(id, title, subTitle, logoUrl, description, startPosition, defaultAudioTrack, audioTracks, defaultSubtrack, subtitleTracks, trickPlayModel, chapters, segments, previousVideo, nextVideo, url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
return listOf(
|
return listOf(
|
||||||
id,
|
currentItem,
|
||||||
title,
|
|
||||||
subTitle,
|
|
||||||
logoUrl,
|
|
||||||
description,
|
description,
|
||||||
startPosition,
|
startPosition,
|
||||||
defaultAudioTrack,
|
defaultAudioTrack,
|
||||||
|
|
@ -453,40 +487,45 @@ private open class VideoPlayerHelperPigeonCodec : StandardMessageCodec() {
|
||||||
}
|
}
|
||||||
130.toByte() -> {
|
130.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
PlayableData.fromList(it)
|
SimpleItemModel.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
131.toByte() -> {
|
131.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
MediaSegment.fromList(it)
|
PlayableData.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
132.toByte() -> {
|
132.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
AudioTrack.fromList(it)
|
MediaSegment.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
133.toByte() -> {
|
133.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
SubtitleTrack.fromList(it)
|
AudioTrack.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
134.toByte() -> {
|
134.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
Chapter.fromList(it)
|
SubtitleTrack.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
135.toByte() -> {
|
135.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
TrickPlayModel.fromList(it)
|
Chapter.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
136.toByte() -> {
|
136.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
StartResult.fromList(it)
|
TrickPlayModel.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
137.toByte() -> {
|
137.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
StartResult.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
138.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
PlaybackState.fromList(it)
|
PlaybackState.fromList(it)
|
||||||
}
|
}
|
||||||
|
|
@ -500,38 +539,42 @@ private open class VideoPlayerHelperPigeonCodec : StandardMessageCodec() {
|
||||||
stream.write(129)
|
stream.write(129)
|
||||||
writeValue(stream, value.raw)
|
writeValue(stream, value.raw)
|
||||||
}
|
}
|
||||||
is PlayableData -> {
|
is SimpleItemModel -> {
|
||||||
stream.write(130)
|
stream.write(130)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is MediaSegment -> {
|
is PlayableData -> {
|
||||||
stream.write(131)
|
stream.write(131)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is AudioTrack -> {
|
is MediaSegment -> {
|
||||||
stream.write(132)
|
stream.write(132)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is SubtitleTrack -> {
|
is AudioTrack -> {
|
||||||
stream.write(133)
|
stream.write(133)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is Chapter -> {
|
is SubtitleTrack -> {
|
||||||
stream.write(134)
|
stream.write(134)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is TrickPlayModel -> {
|
is Chapter -> {
|
||||||
stream.write(135)
|
stream.write(135)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is StartResult -> {
|
is TrickPlayModel -> {
|
||||||
stream.write(136)
|
stream.write(136)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is PlaybackState -> {
|
is StartResult -> {
|
||||||
stream.write(137)
|
stream.write(137)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
|
is PlaybackState -> {
|
||||||
|
stream.write(138)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
else -> super.writeValue(stream, value)
|
else -> super.writeValue(stream, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package nl.jknaapen.fladder.composables.controls
|
package nl.jknaapen.fladder.composables.controls
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
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.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
|
@ -20,11 +23,15 @@ import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import nl.jknaapen.fladder.utility.conditional
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CustomIconButton(
|
internal fun CustomButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
|
@ -38,6 +45,23 @@ internal fun CustomIconButton(
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var isClickAnimationActive by remember { mutableStateOf(false) }
|
||||||
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
isClickAnimationActive = isPressed
|
||||||
|
|
||||||
|
val targetScale = when {
|
||||||
|
isClickAnimationActive -> 0.9f
|
||||||
|
isFocused && enableScaledFocus -> 1.05f
|
||||||
|
else -> 1.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
val animatedScale by animateFloatAsState(
|
||||||
|
targetValue = targetScale,
|
||||||
|
animationSpec = spring(dampingRatio = 0.5f, stiffness = 400f),
|
||||||
|
label = "buttonScaleAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
val currentContentColor by animateColorAsState(
|
val currentContentColor by animateColorAsState(
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
foreGroundFocusedColor
|
foreGroundFocusedColor
|
||||||
|
|
@ -56,9 +80,29 @@ internal fun CustomIconButton(
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.conditional(enableScaledFocus) {
|
.onKeyEvent { event ->
|
||||||
scale(if (isFocused) 1.05f else 1f)
|
if (!enabled || !isFocused) return@onKeyEvent false
|
||||||
|
|
||||||
|
val isDpadClick = event.key == Key.Enter || event.key == Key.DirectionCenter
|
||||||
|
|
||||||
|
if (isDpadClick) {
|
||||||
|
when (event.type) {
|
||||||
|
KeyEventType.KeyDown -> {
|
||||||
|
isClickAnimationActive = true
|
||||||
|
return@onKeyEvent false
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventType.KeyUp -> {
|
||||||
|
isClickAnimationActive = false
|
||||||
|
return@onKeyEvent false
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return@onKeyEvent false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@onKeyEvent false
|
||||||
}
|
}
|
||||||
|
.scale(animatedScale)
|
||||||
.background(currentBackgroundColor, shape = CircleShape)
|
.background(currentBackgroundColor, shape = CircleShape)
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
.onFocusChanged { isFocused = it.isFocused }
|
||||||
.clickable(
|
.clickable(
|
||||||
|
|
@ -19,8 +19,8 @@ fun ItemHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: PlayableData?
|
state: PlayableData?
|
||||||
) {
|
) {
|
||||||
val title = state?.title
|
val title = state?.currentItem?.title
|
||||||
val logoUrl = state?.logoUrl
|
val logoUrl = state?.currentItem?.logoUrl
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ internal fun ProgressBar(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
val progressBarTopLabel = listOf(
|
val progressBarTopLabel = listOf(
|
||||||
playableData?.subTitle,
|
playableData?.currentItem?.subTitle,
|
||||||
endTimeString,
|
endTimeString,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ internal fun BoxScope.SegmentSkipOverlay(
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.safeContentPadding()
|
.safeContentPadding()
|
||||||
) {
|
) {
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.defaultSelected(true),
|
.defaultSelected(true),
|
||||||
|
|
|
||||||
|
|
@ -370,16 +370,16 @@ fun PlaybackButtons(
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
onClick = { VideoPlayerObject.videoPlayerControls?.loadPreviousVideo {} },
|
onClick = { VideoPlayerObject.videoPlayerControls?.loadPreviousVideo {} },
|
||||||
enabled = previousVideo != null,
|
enabled = previousVideo != null,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Filled.Backward,
|
Iconsax.Filled.Backward,
|
||||||
contentDescription = previousVideo,
|
contentDescription = previousVideo?.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
player.seekTo(
|
player.seekTo(
|
||||||
player.currentPosition - backwardSpeed.inWholeMilliseconds
|
player.currentPosition - backwardSpeed.inWholeMilliseconds
|
||||||
|
|
@ -402,7 +402,7 @@ fun PlaybackButtons(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(bottomControlFocusRequester)
|
.focusRequester(bottomControlFocusRequester)
|
||||||
.defaultSelected(true),
|
.defaultSelected(true),
|
||||||
|
|
@ -417,7 +417,7 @@ fun PlaybackButtons(
|
||||||
contentDescription = if (isPlaying) "Pause" else "Play",
|
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
player.seekTo(
|
player.seekTo(
|
||||||
player.currentPosition + forwardSpeed.inWholeMilliseconds
|
player.currentPosition + forwardSpeed.inWholeMilliseconds
|
||||||
|
|
@ -443,13 +443,13 @@ fun PlaybackButtons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
onClick = { VideoPlayerObject.videoPlayerControls?.loadNextVideo {} },
|
onClick = { VideoPlayerObject.videoPlayerControls?.loadNextVideo {} },
|
||||||
enabled = nextVideo != null,
|
enabled = nextVideo != null,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Filled.Forward,
|
Iconsax.Filled.Forward,
|
||||||
contentDescription = nextVideo,
|
contentDescription = nextVideo?.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -465,7 +465,7 @@ internal fun RowScope.LeftButtons(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.Start)
|
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.Start)
|
||||||
) {
|
) {
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
onClick = openChapterSelection,
|
onClick = openChapterSelection,
|
||||||
enabled = chapters?.isNotEmpty() == true
|
enabled = chapters?.isNotEmpty() == true
|
||||||
) {
|
) {
|
||||||
|
|
@ -486,7 +486,7 @@ internal fun RowScope.RightButtons(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.End)
|
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.End)
|
||||||
) {
|
) {
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showAudioDialog.value = true
|
showAudioDialog.value = true
|
||||||
},
|
},
|
||||||
|
|
@ -496,7 +496,7 @@ internal fun RowScope.RightButtons(
|
||||||
contentDescription = "Audio Track",
|
contentDescription = "Audio Track",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showSubDialog.value = true
|
showSubDialog.value = true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -100,6 +101,7 @@ internal fun ChapterSelectionSheet(
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
)
|
)
|
||||||
|
.aspectRatio(1.67f)
|
||||||
.border(
|
.border(
|
||||||
width = 2.dp,
|
width = 2.dp,
|
||||||
color = Color.White.copy(alpha = if (selectedChapter) 0.45f else 0f),
|
color = Color.White.copy(alpha = if (selectedChapter) 0.45f else 0f),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ 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.Iconsax
|
||||||
import io.github.rabehx.iconsax.filled.TickSquare
|
import io.github.rabehx.iconsax.filled.TickSquare
|
||||||
import nl.jknaapen.fladder.composables.controls.CustomIconButton
|
import nl.jknaapen.fladder.composables.controls.CustomButton
|
||||||
import nl.jknaapen.fladder.utility.defaultSelected
|
import nl.jknaapen.fladder.utility.defaultSelected
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -29,7 +29,7 @@ internal fun TrackButton(
|
||||||
val textStyle =
|
val textStyle =
|
||||||
MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
|
MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
CustomIconButton(
|
CustomButton(
|
||||||
backgroundColor = Color.White.copy(alpha = 0.25f),
|
backgroundColor = Color.White.copy(alpha = 0.25f),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(vertical = 6.dp, horizontal = 12.dp)
|
.padding(vertical = 6.dp, horizontal = 12.dp)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
package nl.jknaapen.fladder.composables.overlays
|
||||||
|
|
||||||
|
import AutoNextType
|
||||||
|
import MediaSegmentType
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.mutableIntStateOf
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import io.github.rabehx.iconsax.Iconsax
|
||||||
|
import io.github.rabehx.iconsax.filled.CloseCircle
|
||||||
|
import io.github.rabehx.iconsax.filled.Next
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import nl.jknaapen.fladder.composables.controls.CustomButton
|
||||||
|
import nl.jknaapen.fladder.objects.PlayerSettingsObject
|
||||||
|
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||||
|
import nl.jknaapen.fladder.utility.conditional
|
||||||
|
import nl.jknaapen.fladder.utility.highlightOnFocus
|
||||||
|
import nl.jknaapen.fladder.utility.visible
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun NextUpOverlay(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable BoxScope.(Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val nextVideo by VideoPlayerObject.nextUpVideo.collectAsState(null)
|
||||||
|
|
||||||
|
val nextType by PlayerSettingsObject.autoNextType.collectAsState(AutoNextType.OFF)
|
||||||
|
var disableUntilNextVideo by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (nextType == AutoNextType.OFF || nextVideo == null) {
|
||||||
|
return Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
content(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isConditionMet = showNextUp()
|
||||||
|
|
||||||
|
var nextUpVisible by remember { mutableStateOf(false) }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var timeUntilNextVideo by remember { mutableIntStateOf(30) }
|
||||||
|
|
||||||
|
fun reInitNextUp() {
|
||||||
|
nextUpVisible = false
|
||||||
|
disableUntilNextVideo = false
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isConditionMet) {
|
||||||
|
if (isConditionMet && !nextUpVisible && !disableUntilNextVideo) {
|
||||||
|
nextUpVisible = true
|
||||||
|
timeUntilNextVideo = 30
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
} else if (!isConditionMet) {
|
||||||
|
nextUpVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isBuffering by VideoPlayerObject.buffering.collectAsState(true)
|
||||||
|
|
||||||
|
LaunchedEffect(nextVideo) {
|
||||||
|
reInitNextUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadNextVideo() {
|
||||||
|
if (isBuffering) return
|
||||||
|
VideoPlayerObject.videoPlayerControls?.loadNextVideo {}
|
||||||
|
reInitNextUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
val showNextUp = nextUpVisible && !disableUntilNextVideo
|
||||||
|
|
||||||
|
LaunchedEffect(showNextUp) {
|
||||||
|
if (showNextUp) {
|
||||||
|
while (timeUntilNextVideo > 0 && isActive) {
|
||||||
|
delay(1000)
|
||||||
|
timeUntilNextVideo -= 1
|
||||||
|
}
|
||||||
|
loadNextVideo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = LocalActivity.current
|
||||||
|
|
||||||
|
val animatedDp by animateDpAsState(if (showNextUp) 16.dp else 0.dp, label = "paddingDp")
|
||||||
|
val animatedSizeFraction by animateFloatAsState(
|
||||||
|
if (showNextUp) 0.6f else 1f,
|
||||||
|
label = "sizeFraction"
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.background(
|
||||||
|
color = Color(0xFF0E0E0E),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(animatedDp)
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.fillMaxSize(fraction = animatedSizeFraction)
|
||||||
|
.conditional(showNextUp) {
|
||||||
|
highlightOnFocus(
|
||||||
|
width = if (showNextUp) 2.dp else 0.dp,
|
||||||
|
useClip = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
enabled = showNextUp,
|
||||||
|
onClick = {
|
||||||
|
disableUntilNextVideo = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
content(!showNextUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentHeight()
|
||||||
|
.fillMaxWidth(fraction = 0.4f)
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.visible(showNextUp)
|
||||||
|
.padding(16.dp)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Next-up in $timeUntilNextVideo seconds",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(alignment = Alignment.CenterHorizontally)
|
||||||
|
.fillMaxWidth(fraction = 0.1f)
|
||||||
|
.heightIn(2.dp)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
MediaInfo()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
CustomButton(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
onClick = ::loadNextVideo,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Next")
|
||||||
|
Icon(Iconsax.Filled.Next, contentDescription = "Play next video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity?.let {
|
||||||
|
CustomButton(
|
||||||
|
onClick = {
|
||||||
|
reInitNextUp()
|
||||||
|
activity.finish()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Close")
|
||||||
|
Icon(Iconsax.Filled.CloseCircle, contentDescription = "Close Icon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MediaInfo() {
|
||||||
|
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
|
val nextUpVideo by VideoPlayerObject.nextUpVideo.collectAsState(null)
|
||||||
|
|
||||||
|
nextUpVideo?.let { video ->
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
video.title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = onSurfaceColor
|
||||||
|
)
|
||||||
|
if (video.title != video.subTitle && video.subTitle != null)
|
||||||
|
Text(
|
||||||
|
video.subTitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = onSurfaceColor.copy(alpha = 0.65f)
|
||||||
|
)
|
||||||
|
AsyncImage(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(ratio = 1.67f)
|
||||||
|
.clip(RoundedCornerShape(16.dp)),
|
||||||
|
model = video.primaryPoster,
|
||||||
|
contentDescription = "ItemPoster"
|
||||||
|
)
|
||||||
|
|
||||||
|
video.overview?.let { overview ->
|
||||||
|
Text(
|
||||||
|
overview,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 4,
|
||||||
|
color = onSurfaceColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun showNextUp(): Boolean {
|
||||||
|
val nextType by PlayerSettingsObject.autoNextType.collectAsState(AutoNextType.OFF)
|
||||||
|
val durationMs by VideoPlayerObject.duration.collectAsState(0L)
|
||||||
|
val positionMs by VideoPlayerObject.position.collectAsState(0L)
|
||||||
|
val buffering by VideoPlayerObject.buffering.collectAsState(true)
|
||||||
|
|
||||||
|
val videoDuration = durationMs.toDuration(DurationUnit.MILLISECONDS)
|
||||||
|
val videoPosition = positionMs.toDuration(DurationUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
if (nextType == AutoNextType.OFF || videoDuration < 40.seconds || buffering) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val playbackData by VideoPlayerObject.implementation.playbackData.collectAsState()
|
||||||
|
val credits =
|
||||||
|
(playbackData?.segments ?: listOf()).firstOrNull { it.type == MediaSegmentType.OUTRO }
|
||||||
|
|
||||||
|
val timeToVideoEnd = (videoDuration - videoPosition).absoluteValue
|
||||||
|
val nearEndOfVideo = timeToVideoEnd < 32.seconds
|
||||||
|
|
||||||
|
when (nextType) {
|
||||||
|
AutoNextType.STATIC -> {
|
||||||
|
if (nearEndOfVideo) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoNextType.SMART -> {
|
||||||
|
if (credits != null) {
|
||||||
|
val maxTimePct = 90.0
|
||||||
|
val resumeDuration = videoDuration * (maxTimePct / 100)
|
||||||
|
|
||||||
|
val creditsEnd = credits.end.toDuration(DurationUnit.MILLISECONDS)
|
||||||
|
val creditsStart = credits.start.toDuration(DurationUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
val timeLeftAfterCredits = videoDuration - creditsEnd
|
||||||
|
|
||||||
|
if (creditsEnd > resumeDuration && timeLeftAfterCredits < 30.seconds) {
|
||||||
|
if (videoPosition >= creditsStart) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if (nearEndOfVideo) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (nearEndOfVideo) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoNextType.OFF -> return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -48,8 +48,8 @@ class VideoPlayerImplementation(
|
||||||
val subTitles = playbackData.value?.subtitleTracks ?: listOf()
|
val subTitles = playbackData.value?.subtitleTracks ?: listOf()
|
||||||
val mediaItem = MediaItem.Builder()
|
val mediaItem = MediaItem.Builder()
|
||||||
.setUri(url)
|
.setUri(url)
|
||||||
.setTag(playbackData.value?.title)
|
.setTag(playbackData.value?.currentItem?.title)
|
||||||
.setMediaId(playbackData.value?.id ?: "")
|
.setMediaId(playbackData.value?.currentItem?.id ?: "")
|
||||||
.setSubtitleConfigurations(
|
.setSubtitleConfigurations(
|
||||||
subTitles.filter { it.external && !it.url.isNullOrEmpty() }.map { sub ->
|
subTitles.filter { it.external && !it.url.isNullOrEmpty() }.map { sub ->
|
||||||
MediaItem.SubtitleConfiguration.Builder(sub.url!!.toUri())
|
MediaItem.SubtitleConfiguration.Builder(sub.url!!.toUri())
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package nl.jknaapen.fladder.objects
|
package nl.jknaapen.fladder.objects
|
||||||
|
|
||||||
|
import AutoNextType
|
||||||
import PlayerSettings
|
import PlayerSettings
|
||||||
import PlayerSettingsPigeon
|
import PlayerSettingsPigeon
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -28,6 +29,10 @@ object PlayerSettingsObject : PlayerSettingsPigeon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val autoNextType = settings.map { settings ->
|
||||||
|
settings?.autoNextType ?: AutoNextType.OFF
|
||||||
|
}
|
||||||
|
|
||||||
override fun sendPlayerSettings(playerSettings: PlayerSettings) {
|
override fun sendPlayerSettings(playerSettings: PlayerSettings) {
|
||||||
settings.value = playerSettings
|
settings.value = playerSettings
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ object VideoPlayerObject {
|
||||||
|
|
||||||
val chapters = implementation.playbackData.map { it?.chapters }
|
val chapters = implementation.playbackData.map { it?.chapters }
|
||||||
|
|
||||||
|
val nextUpVideo = implementation.playbackData.map { it?.nextVideo }
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
val endTime = combine(position, duration) { pos, dur ->
|
val endTime = combine(position, duration) { pos, dur ->
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,14 @@ import PlaybackState
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
|
@ -35,11 +33,14 @@ import androidx.media3.ui.CaptionStyleCompat
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import io.github.peerless2012.ass.media.kt.buildWithAssSupport
|
import io.github.peerless2012.ass.media.kt.buildWithAssSupport
|
||||||
import io.github.peerless2012.ass.media.type.AssRenderType
|
import io.github.peerless2012.ass.media.type.AssRenderType
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import nl.jknaapen.fladder.composables.overlays.NextUpOverlay
|
||||||
import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks
|
import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks
|
||||||
import nl.jknaapen.fladder.objects.PlayerSettingsObject
|
import nl.jknaapen.fladder.objects.PlayerSettingsObject
|
||||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||||
import nl.jknaapen.fladder.utility.getAudioTracks
|
import nl.jknaapen.fladder.utility.getAudioTracks
|
||||||
import nl.jknaapen.fladder.utility.getSubtitleTracks
|
import nl.jknaapen.fladder.utility.getSubtitleTracks
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
val LocalPlayer = compositionLocalOf<ExoPlayer?> { null }
|
val LocalPlayer = compositionLocalOf<ExoPlayer?> { null }
|
||||||
|
|
||||||
|
|
@ -108,9 +109,29 @@ internal fun ExoPlayer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updatePlaybackState() {
|
||||||
|
videoHost.setPlaybackState(
|
||||||
|
PlaybackState(
|
||||||
|
position = exoPlayer.currentPosition,
|
||||||
|
buffered = exoPlayer.bufferedPosition,
|
||||||
|
duration = exoPlayer.duration,
|
||||||
|
playing = exoPlayer.isPlaying,
|
||||||
|
buffering = exoPlayer.playbackState == Player.STATE_BUFFERING,
|
||||||
|
completed = exoPlayer.playbackState == Player.STATE_ENDED,
|
||||||
|
failed = exoPlayer.playbackState == Player.STATE_IDLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(exoPlayer) {
|
||||||
|
while (true) {
|
||||||
|
updatePlaybackState()
|
||||||
|
delay(1.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(exoPlayer) {
|
DisposableEffect(exoPlayer) {
|
||||||
val listener = object : Player.Listener {
|
val listener = object : Player.Listener {
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
videoHost.setPlaybackState(
|
videoHost.setPlaybackState(
|
||||||
PlaybackState(
|
PlaybackState(
|
||||||
|
|
@ -127,17 +148,7 @@ internal fun ExoPlayer(
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
super.onEvents(player, events)
|
super.onEvents(player, events)
|
||||||
videoHost.setPlaybackState(
|
updatePlaybackState()
|
||||||
PlaybackState(
|
|
||||||
position = exoPlayer.currentPosition,
|
|
||||||
buffered = exoPlayer.bufferedPosition,
|
|
||||||
duration = exoPlayer.duration,
|
|
||||||
playing = exoPlayer.isPlaying,
|
|
||||||
buffering = exoPlayer.playbackState == Player.STATE_BUFFERING,
|
|
||||||
completed = exoPlayer.playbackState == Player.STATE_ENDED,
|
|
||||||
failed = exoPlayer.playbackState == Player.STATE_IDLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
|
|
@ -168,13 +179,13 @@ internal fun ExoPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Box(
|
NextUpOverlay(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(Color.Black)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) { showControls ->
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
factory = {
|
factory = {
|
||||||
PlayerView(it).apply {
|
PlayerView(it).apply {
|
||||||
player = exoPlayer
|
player = exoPlayer
|
||||||
|
|
@ -198,8 +209,9 @@ internal fun ExoPlayer(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
CompositionLocalProvider(LocalPlayer provides exoPlayer) {
|
if (showControls)
|
||||||
controls(exoPlayer)
|
CompositionLocalProvider(LocalPlayer provides exoPlayer) {
|
||||||
}
|
controls(exoPlayer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,30 +30,41 @@ import androidx.compose.ui.unit.dp
|
||||||
fun Modifier.highlightOnFocus(
|
fun Modifier.highlightOnFocus(
|
||||||
color: Color = Color.White.copy(alpha = 0.85f),
|
color: Color = Color.White.copy(alpha = 0.85f),
|
||||||
width: Dp = 1.5.dp,
|
width: Dp = 1.5.dp,
|
||||||
shape: Shape = RoundedCornerShape(16.dp)
|
shape: Shape = RoundedCornerShape(16.dp),
|
||||||
|
useClip: Boolean = true,
|
||||||
): Modifier = composed {
|
): Modifier = composed {
|
||||||
var hasFocus by remember { mutableStateOf(false) }
|
var hasFocus by remember { mutableStateOf(false) }
|
||||||
val highlightModifier = remember {
|
val highlightModifier = remember {
|
||||||
if (width != 0.dp) {
|
if (!useClip) {
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(
|
.background(
|
||||||
color = color.copy(alpha = 0.25f),
|
color = color.copy(alpha = 0.25f),
|
||||||
shape = shape,
|
|
||||||
)
|
)
|
||||||
.border(
|
.border(
|
||||||
width = width,
|
width = width,
|
||||||
color = color.copy(alpha = 0.5f),
|
color = color.copy(alpha = 0.5f),
|
||||||
shape = shape
|
|
||||||
)
|
)
|
||||||
} else {
|
} else
|
||||||
Modifier
|
if (width != 0.dp) {
|
||||||
.clip(RoundedCornerShape(8.dp))
|
Modifier
|
||||||
.background(
|
.clip(shape)
|
||||||
color = color.copy(alpha = 0.25f),
|
.background(
|
||||||
shape = shape,
|
color = color.copy(alpha = 0.25f),
|
||||||
)
|
shape = shape,
|
||||||
}
|
)
|
||||||
|
.border(
|
||||||
|
width = width,
|
||||||
|
color = color.copy(alpha = 0.5f),
|
||||||
|
shape = shape
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.clip(shape)
|
||||||
|
.background(
|
||||||
|
color = color.copy(alpha = 0.25f),
|
||||||
|
shape = shape,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this
|
this
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,18 @@ package nl.jknaapen.fladder.utility
|
||||||
import AudioTrack
|
import AudioTrack
|
||||||
import Chapter
|
import Chapter
|
||||||
import PlayableData
|
import PlayableData
|
||||||
|
import SimpleItemModel
|
||||||
import SubtitleTrack
|
import SubtitleTrack
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
val testPlaybackData = PlayableData(
|
val testPlaybackData = PlayableData(
|
||||||
id = "8lsf8234l99sdf923lsd8f23j98j",
|
currentItem = SimpleItemModel(
|
||||||
title = "Big buck bunny",
|
id = "8lsf8234l99sdf923lsd8f23j98j",
|
||||||
subTitle = "Episode 1x2",
|
title = "Big buck bunny",
|
||||||
|
subTitle = "Episode 1x2",
|
||||||
|
primaryPoster = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.ytimg.com%2Fvi%2Faqz-KE-bpKQ%2Fmaxresdefault.jpg&f=1&nofb=1&ipt=4e375598bf8cc78e681ee62de9111dea32b85972ae756e40a1eddac01aa79f80"
|
||||||
|
),
|
||||||
startPosition = 0,
|
startPosition = 0,
|
||||||
description = "Short description of the movie that is being watched",
|
description = "Short description of the movie that is being watched",
|
||||||
defaultSubtrack = 1,
|
defaultSubtrack = 1,
|
||||||
|
|
@ -88,7 +92,12 @@ val testPlaybackData = PlayableData(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
nextVideo = null,
|
nextVideo = null,
|
||||||
previousVideo = "Previous episode name",
|
previousVideo = SimpleItemModel(
|
||||||
|
id = "8lsf8234l99sdf923lsd8f23j98j",
|
||||||
|
title = "Big buck bunny",
|
||||||
|
subTitle = "Episode 1x26",
|
||||||
|
primaryPoster = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.ytimg.com%2Fvi%2Faqz-KE-bpKQ%2Fmaxresdefault.jpg&f=1&nofb=1&ipt=4e375598bf8cc78e681ee62de9111dea32b85972ae756e40a1eddac01aa79f80"
|
||||||
|
),
|
||||||
segments = listOf(),
|
segments = listOf(),
|
||||||
url = "https://github.com/ietf-wg-cellar/matroska-test-files/raw/refs/heads/master/test_files/test5.mkv",
|
url = "https://github.com/ietf-wg-cellar/matroska-test-files/raw/refs/heads/master/test_files/test5.mkv",
|
||||||
)
|
)
|
||||||
|
|
@ -30,6 +30,7 @@ import 'package:fladder/screens/details_screens/episode_detail_screen.dart';
|
||||||
import 'package:fladder/screens/details_screens/season_detail_screen.dart';
|
import 'package:fladder/screens/details_screens/season_detail_screen.dart';
|
||||||
import 'package:fladder/screens/library_search/library_search_screen.dart';
|
import 'package:fladder/screens/library_search/library_search_screen.dart';
|
||||||
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
||||||
|
import 'package:fladder/src/video_player_helper.g.dart' show SimpleItemModel;
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/util/string_extensions.dart';
|
import 'package:fladder/util/string_extensions.dart';
|
||||||
|
|
||||||
|
|
@ -233,6 +234,17 @@ class ItemBaseModel with ItemBaseModelMappable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SimpleItemModel toSimpleItem(BuildContext? context) {
|
||||||
|
return SimpleItemModel(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
subTitle: context != null ? label(context) : null,
|
||||||
|
overview: overview.summary,
|
||||||
|
logoUrl: images?.logo?.path,
|
||||||
|
primaryPoster: images?.primary?.path ?? getPosters?.primary?.path ?? "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
FladderItemType get type => switch (this) {
|
FladderItemType get type => switch (this) {
|
||||||
MovieModel _ => FladderItemType.movie,
|
MovieModel _ => FladderItemType.movie,
|
||||||
SeriesModel _ => FladderItemType.series,
|
SeriesModel _ => FladderItemType.series,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
import 'package:fladder/providers/settings/client_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';
|
||||||
|
|
@ -42,6 +43,11 @@ final pigeonPlayerSettingsSyncProvider = Provider<void>((ref) {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
themeColor: color,
|
themeColor: color,
|
||||||
|
autoNextType: switch (value.nextVideoType) {
|
||||||
|
AutoNextType.off => pigeon.AutoNextType.off,
|
||||||
|
AutoNextType.static => pigeon.AutoNextType.static,
|
||||||
|
AutoNextType.smart => pigeon.AutoNextType.smart,
|
||||||
|
},
|
||||||
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
|
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
|
||||||
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
|
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -377,39 +377,42 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"),
|
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[
|
Column(
|
||||||
Column(
|
children: [
|
||||||
children: [
|
SettingsListTile(
|
||||||
SettingsListTile(
|
label: Text(context.localized.settingsAutoNextTitle),
|
||||||
label: Text(context.localized.settingsAutoNextTitle),
|
subLabel: Text(context.localized.settingsAutoNextDesc),
|
||||||
subLabel: Text(context.localized.settingsAutoNextDesc),
|
trailing: EnumBox(
|
||||||
trailing: EnumBox(
|
current: ref.watch(
|
||||||
current: ref.watch(
|
videoPlayerSettingsProvider.select(
|
||||||
videoPlayerSettingsProvider.select(
|
(value) => value.nextVideoType.label(context),
|
||||||
(value) => value.nextVideoType.label(context),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
itemBuilder: (context) => AutoNextType.values
|
|
||||||
.map(
|
|
||||||
(entry) => ItemActionButton(
|
|
||||||
label: Text(entry.label(context)),
|
|
||||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
|
||||||
videoSettings.copyWith(nextVideoType: entry),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
|
itemBuilder: (context) => AutoNextType.values
|
||||||
|
.map(
|
||||||
|
(entry) => ItemActionButton(
|
||||||
|
label: Text(entry.label(context)),
|
||||||
|
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||||
|
videoSettings.copyWith(nextVideoType: entry),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
AnimatedFadeSize(
|
),
|
||||||
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
|
AnimatedFadeSize(
|
||||||
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
|
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
|
||||||
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
|
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
|
||||||
_ => const SizedBox.shrink(),
|
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
|
||||||
},
|
_ => const SizedBox.shrink(),
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode)
|
),
|
||||||
|
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[
|
||||||
|
if (!AdaptiveLayout.of(context).isDesktop &&
|
||||||
|
!kIsWeb &&
|
||||||
|
!ref.read(argumentsStateProvider).htpcMode &&
|
||||||
|
videoSettings.wantedPlayer != PlayerOptions.nativePlayer)
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.playerSettingsOrientationTitle),
|
label: Text(context.localized.playerSettingsOrientationTitle),
|
||||||
subLabel: Text(context.localized.playerSettingsOrientationDesc),
|
subLabel: Text(context.localized.playerSettingsOrientationDesc),
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ bool _deepEquals(Object? a, Object? b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum AutoNextType {
|
||||||
|
off,
|
||||||
|
static,
|
||||||
|
smart,
|
||||||
|
}
|
||||||
|
|
||||||
enum SegmentType {
|
enum SegmentType {
|
||||||
commercial,
|
commercial,
|
||||||
preview,
|
preview,
|
||||||
|
|
@ -50,6 +56,7 @@ class PlayerSettings {
|
||||||
this.themeColor,
|
this.themeColor,
|
||||||
required this.skipForward,
|
required this.skipForward,
|
||||||
required this.skipBackward,
|
required this.skipBackward,
|
||||||
|
required this.autoNextType,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool enableTunneling;
|
bool enableTunneling;
|
||||||
|
|
@ -62,6 +69,8 @@ class PlayerSettings {
|
||||||
|
|
||||||
int skipBackward;
|
int skipBackward;
|
||||||
|
|
||||||
|
AutoNextType autoNextType;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
enableTunneling,
|
enableTunneling,
|
||||||
|
|
@ -69,6 +78,7 @@ class PlayerSettings {
|
||||||
themeColor,
|
themeColor,
|
||||||
skipForward,
|
skipForward,
|
||||||
skipBackward,
|
skipBackward,
|
||||||
|
autoNextType,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +93,7 @@ class PlayerSettings {
|
||||||
themeColor: result[2] as int?,
|
themeColor: result[2] as int?,
|
||||||
skipForward: result[3]! as int,
|
skipForward: result[3]! as int,
|
||||||
skipBackward: result[4]! as int,
|
skipBackward: result[4]! as int,
|
||||||
|
autoNextType: result[5]! as AutoNextType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,14 +123,17 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||||
if (value is int) {
|
if (value is int) {
|
||||||
buffer.putUint8(4);
|
buffer.putUint8(4);
|
||||||
buffer.putInt64(value);
|
buffer.putInt64(value);
|
||||||
} else if (value is SegmentType) {
|
} else if (value is AutoNextType) {
|
||||||
buffer.putUint8(129);
|
buffer.putUint8(129);
|
||||||
writeValue(buffer, value.index);
|
writeValue(buffer, value.index);
|
||||||
} else if (value is SegmentSkip) {
|
} else if (value is SegmentType) {
|
||||||
buffer.putUint8(130);
|
buffer.putUint8(130);
|
||||||
writeValue(buffer, value.index);
|
writeValue(buffer, value.index);
|
||||||
} else if (value is PlayerSettings) {
|
} else if (value is SegmentSkip) {
|
||||||
buffer.putUint8(131);
|
buffer.putUint8(131);
|
||||||
|
writeValue(buffer, value.index);
|
||||||
|
} else if (value is PlayerSettings) {
|
||||||
|
buffer.putUint8(132);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else {
|
} else {
|
||||||
super.writeValue(buffer, value);
|
super.writeValue(buffer, value);
|
||||||
|
|
@ -131,11 +145,14 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 129:
|
case 129:
|
||||||
final int? value = readValue(buffer) as int?;
|
final int? value = readValue(buffer) as int?;
|
||||||
return value == null ? null : SegmentType.values[value];
|
return value == null ? null : AutoNextType.values[value];
|
||||||
case 130:
|
case 130:
|
||||||
final int? value = readValue(buffer) as int?;
|
final int? value = readValue(buffer) as int?;
|
||||||
return value == null ? null : SegmentSkip.values[value];
|
return value == null ? null : SegmentType.values[value];
|
||||||
case 131:
|
case 131:
|
||||||
|
final int? value = readValue(buffer) as int?;
|
||||||
|
return value == null ? null : SegmentSkip.values[value];
|
||||||
|
case 132:
|
||||||
return PlayerSettings.decode(readValue(buffer)!);
|
return PlayerSettings.decode(readValue(buffer)!);
|
||||||
default:
|
default:
|
||||||
return super.readValueOfType(type, buffer);
|
return super.readValueOfType(type, buffer);
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,75 @@ enum MediaSegmentType {
|
||||||
outro,
|
outro,
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayableData {
|
class SimpleItemModel {
|
||||||
PlayableData({
|
SimpleItemModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.subTitle,
|
this.subTitle,
|
||||||
|
this.overview,
|
||||||
this.logoUrl,
|
this.logoUrl,
|
||||||
|
required this.primaryPoster,
|
||||||
|
});
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
String title;
|
||||||
|
|
||||||
|
String? subTitle;
|
||||||
|
|
||||||
|
String? overview;
|
||||||
|
|
||||||
|
String? logoUrl;
|
||||||
|
|
||||||
|
String primaryPoster;
|
||||||
|
|
||||||
|
List<Object?> _toList() {
|
||||||
|
return <Object?>[
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
overview,
|
||||||
|
logoUrl,
|
||||||
|
primaryPoster,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object encode() {
|
||||||
|
return _toList(); }
|
||||||
|
|
||||||
|
static SimpleItemModel decode(Object result) {
|
||||||
|
result as List<Object?>;
|
||||||
|
return SimpleItemModel(
|
||||||
|
id: result[0]! as String,
|
||||||
|
title: result[1]! as String,
|
||||||
|
subTitle: result[2] as String?,
|
||||||
|
overview: result[3] as String?,
|
||||||
|
logoUrl: result[4] as String?,
|
||||||
|
primaryPoster: result[5]! as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is! SimpleItemModel || other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return _deepEquals(encode(), other.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
int get hashCode => Object.hashAll(_toList())
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayableData {
|
||||||
|
PlayableData({
|
||||||
|
required this.currentItem,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.startPosition,
|
required this.startPosition,
|
||||||
required this.defaultAudioTrack,
|
required this.defaultAudioTrack,
|
||||||
|
|
@ -67,13 +130,7 @@ class PlayableData {
|
||||||
required this.url,
|
required this.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
SimpleItemModel currentItem;
|
||||||
|
|
||||||
String title;
|
|
||||||
|
|
||||||
String? subTitle;
|
|
||||||
|
|
||||||
String? logoUrl;
|
|
||||||
|
|
||||||
String description;
|
String description;
|
||||||
|
|
||||||
|
|
@ -93,18 +150,15 @@ class PlayableData {
|
||||||
|
|
||||||
List<MediaSegment> segments;
|
List<MediaSegment> segments;
|
||||||
|
|
||||||
String? previousVideo;
|
SimpleItemModel? previousVideo;
|
||||||
|
|
||||||
String? nextVideo;
|
SimpleItemModel? nextVideo;
|
||||||
|
|
||||||
String url;
|
String url;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
id,
|
currentItem,
|
||||||
title,
|
|
||||||
subTitle,
|
|
||||||
logoUrl,
|
|
||||||
description,
|
description,
|
||||||
startPosition,
|
startPosition,
|
||||||
defaultAudioTrack,
|
defaultAudioTrack,
|
||||||
|
|
@ -126,22 +180,19 @@ class PlayableData {
|
||||||
static PlayableData decode(Object result) {
|
static PlayableData decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
return PlayableData(
|
return PlayableData(
|
||||||
id: result[0]! as String,
|
currentItem: result[0]! as SimpleItemModel,
|
||||||
title: result[1]! as String,
|
description: result[1]! as String,
|
||||||
subTitle: result[2] as String?,
|
startPosition: result[2]! as int,
|
||||||
logoUrl: result[3] as String?,
|
defaultAudioTrack: result[3]! as int,
|
||||||
description: result[4]! as String,
|
audioTracks: (result[4] as List<Object?>?)!.cast<AudioTrack>(),
|
||||||
startPosition: result[5]! as int,
|
defaultSubtrack: result[5]! as int,
|
||||||
defaultAudioTrack: result[6]! as int,
|
subtitleTracks: (result[6] as List<Object?>?)!.cast<SubtitleTrack>(),
|
||||||
audioTracks: (result[7] as List<Object?>?)!.cast<AudioTrack>(),
|
trickPlayModel: result[7] as TrickPlayModel?,
|
||||||
defaultSubtrack: result[8]! as int,
|
chapters: (result[8] as List<Object?>?)!.cast<Chapter>(),
|
||||||
subtitleTracks: (result[9] as List<Object?>?)!.cast<SubtitleTrack>(),
|
segments: (result[9] as List<Object?>?)!.cast<MediaSegment>(),
|
||||||
trickPlayModel: result[10] as TrickPlayModel?,
|
previousVideo: result[10] as SimpleItemModel?,
|
||||||
chapters: (result[11] as List<Object?>?)!.cast<Chapter>(),
|
nextVideo: result[11] as SimpleItemModel?,
|
||||||
segments: (result[12] as List<Object?>?)!.cast<MediaSegment>(),
|
url: result[12]! as String,
|
||||||
previousVideo: result[13] as String?,
|
|
||||||
nextVideo: result[14] as String?,
|
|
||||||
url: result[15]! as String,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,30 +647,33 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||||
} else if (value is MediaSegmentType) {
|
} else if (value is MediaSegmentType) {
|
||||||
buffer.putUint8(129);
|
buffer.putUint8(129);
|
||||||
writeValue(buffer, value.index);
|
writeValue(buffer, value.index);
|
||||||
} else if (value is PlayableData) {
|
} else if (value is SimpleItemModel) {
|
||||||
buffer.putUint8(130);
|
buffer.putUint8(130);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is MediaSegment) {
|
} else if (value is PlayableData) {
|
||||||
buffer.putUint8(131);
|
buffer.putUint8(131);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is AudioTrack) {
|
} else if (value is MediaSegment) {
|
||||||
buffer.putUint8(132);
|
buffer.putUint8(132);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is SubtitleTrack) {
|
} else if (value is AudioTrack) {
|
||||||
buffer.putUint8(133);
|
buffer.putUint8(133);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is Chapter) {
|
} else if (value is SubtitleTrack) {
|
||||||
buffer.putUint8(134);
|
buffer.putUint8(134);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is TrickPlayModel) {
|
} else if (value is Chapter) {
|
||||||
buffer.putUint8(135);
|
buffer.putUint8(135);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is StartResult) {
|
} else if (value is TrickPlayModel) {
|
||||||
buffer.putUint8(136);
|
buffer.putUint8(136);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is PlaybackState) {
|
} else if (value is StartResult) {
|
||||||
buffer.putUint8(137);
|
buffer.putUint8(137);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
|
} else if (value is PlaybackState) {
|
||||||
|
buffer.putUint8(138);
|
||||||
|
writeValue(buffer, value.encode());
|
||||||
} else {
|
} else {
|
||||||
super.writeValue(buffer, value);
|
super.writeValue(buffer, value);
|
||||||
}
|
}
|
||||||
|
|
@ -632,20 +686,22 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||||
final int? value = readValue(buffer) as int?;
|
final int? value = readValue(buffer) as int?;
|
||||||
return value == null ? null : MediaSegmentType.values[value];
|
return value == null ? null : MediaSegmentType.values[value];
|
||||||
case 130:
|
case 130:
|
||||||
return PlayableData.decode(readValue(buffer)!);
|
return SimpleItemModel.decode(readValue(buffer)!);
|
||||||
case 131:
|
case 131:
|
||||||
return MediaSegment.decode(readValue(buffer)!);
|
return PlayableData.decode(readValue(buffer)!);
|
||||||
case 132:
|
case 132:
|
||||||
return AudioTrack.decode(readValue(buffer)!);
|
return MediaSegment.decode(readValue(buffer)!);
|
||||||
case 133:
|
case 133:
|
||||||
return SubtitleTrack.decode(readValue(buffer)!);
|
return AudioTrack.decode(readValue(buffer)!);
|
||||||
case 134:
|
case 134:
|
||||||
return Chapter.decode(readValue(buffer)!);
|
return SubtitleTrack.decode(readValue(buffer)!);
|
||||||
case 135:
|
case 135:
|
||||||
return TrickPlayModel.decode(readValue(buffer)!);
|
return Chapter.decode(readValue(buffer)!);
|
||||||
case 136:
|
case 136:
|
||||||
return StartResult.decode(readValue(buffer)!);
|
return TrickPlayModel.decode(readValue(buffer)!);
|
||||||
case 137:
|
case 137:
|
||||||
|
return StartResult.decode(readValue(buffer)!);
|
||||||
|
case 138:
|
||||||
return PlaybackState.decode(readValue(buffer)!);
|
return PlaybackState.decode(readValue(buffer)!);
|
||||||
default:
|
default:
|
||||||
return super.readValueOfType(type, buffer);
|
return super.readValueOfType(type, buffer);
|
||||||
|
|
|
||||||
|
|
@ -100,15 +100,12 @@ class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback {
|
||||||
Duration startPosition,
|
Duration startPosition,
|
||||||
) async {
|
) async {
|
||||||
final playableData = PlayableData(
|
final playableData = PlayableData(
|
||||||
id: model.item.id,
|
currentItem: model.item.toSimpleItem(context),
|
||||||
title: model.item.title,
|
|
||||||
subTitle: context != null ? model.item.label(context) : "",
|
|
||||||
logoUrl: model.item.getPosters?.logo?.path,
|
|
||||||
startPosition: startPosition.inMilliseconds,
|
startPosition: startPosition.inMilliseconds,
|
||||||
description: model.item.overview.summary,
|
description: model.item.overview.summary,
|
||||||
defaultAudioTrack: model.mediaStreams?.defaultAudioStreamIndex ?? 1,
|
defaultAudioTrack: model.mediaStreams?.defaultAudioStreamIndex ?? 1,
|
||||||
nextVideo: model.nextVideo?.name,
|
nextVideo: model.nextVideo?.toSimpleItem(context),
|
||||||
previousVideo: model.previousVideo?.name,
|
previousVideo: model.previousVideo?.toSimpleItem(context),
|
||||||
audioTracks: model.audioStreams
|
audioTracks: model.audioStreams
|
||||||
?.map(
|
?.map(
|
||||||
(audio) => AudioTrack(
|
(audio) => AudioTrack(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class PlayerSettings {
|
||||||
final int? themeColor;
|
final int? themeColor;
|
||||||
final int skipForward;
|
final int skipForward;
|
||||||
final int skipBackward;
|
final int skipBackward;
|
||||||
|
final AutoNextType autoNextType;
|
||||||
|
|
||||||
const PlayerSettings({
|
const PlayerSettings({
|
||||||
required this.enableTunneling,
|
required this.enableTunneling,
|
||||||
|
|
@ -25,9 +26,16 @@ class PlayerSettings {
|
||||||
required this.themeColor,
|
required this.themeColor,
|
||||||
required this.skipForward,
|
required this.skipForward,
|
||||||
required this.skipBackward,
|
required this.skipBackward,
|
||||||
|
required this.autoNextType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AutoNextType {
|
||||||
|
off,
|
||||||
|
static,
|
||||||
|
smart,
|
||||||
|
}
|
||||||
|
|
||||||
enum SegmentType {
|
enum SegmentType {
|
||||||
commercial,
|
commercial,
|
||||||
preview,
|
preview,
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,26 @@ import 'package:pigeon/pigeon.dart';
|
||||||
dartPackageName: 'nl_jknaapen_fladder.video',
|
dartPackageName: 'nl_jknaapen_fladder.video',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
class PlayableData {
|
class SimpleItemModel {
|
||||||
final String id;
|
final String id;
|
||||||
final String title;
|
final String title;
|
||||||
final String? subTitle;
|
final String? subTitle;
|
||||||
|
final String? overview;
|
||||||
final String? logoUrl;
|
final String? logoUrl;
|
||||||
|
final String primaryPoster;
|
||||||
|
|
||||||
|
const SimpleItemModel({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
this.subTitle,
|
||||||
|
this.overview,
|
||||||
|
this.logoUrl,
|
||||||
|
required this.primaryPoster,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayableData {
|
||||||
|
final SimpleItemModel currentItem;
|
||||||
final String description;
|
final String description;
|
||||||
final int startPosition;
|
final int startPosition;
|
||||||
final int defaultAudioTrack;
|
final int defaultAudioTrack;
|
||||||
|
|
@ -23,15 +38,12 @@ class PlayableData {
|
||||||
final TrickPlayModel? trickPlayModel;
|
final TrickPlayModel? trickPlayModel;
|
||||||
final List<Chapter> chapters;
|
final List<Chapter> chapters;
|
||||||
final List<MediaSegment> segments;
|
final List<MediaSegment> segments;
|
||||||
final String? previousVideo;
|
final SimpleItemModel? previousVideo;
|
||||||
final String? nextVideo;
|
final SimpleItemModel? nextVideo;
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
PlayableData({
|
PlayableData({
|
||||||
required this.id,
|
required this.currentItem,
|
||||||
required this.title,
|
|
||||||
this.subTitle,
|
|
||||||
this.logoUrl,
|
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.startPosition,
|
required this.startPosition,
|
||||||
required this.defaultAudioTrack,
|
required this.defaultAudioTrack,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue