mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
fix: Lots of small adjustments and changes to native player
This commit is contained in:
parent
360b87dacb
commit
b4e68c9e15
14 changed files with 197 additions and 156 deletions
|
|
@ -62,6 +62,7 @@
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:launchMode="singleTop"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,11 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity {
|
||||||
override fun launchActivity(callback: (Result<StartResult>) -> Unit) {
|
override fun launchActivity(callback: (Result<StartResult>) -> Unit) {
|
||||||
try {
|
try {
|
||||||
videoPlayerCallback = callback
|
videoPlayerCallback = callback
|
||||||
val intent = Intent(this, VideoPlayerActivity::class.java)
|
|
||||||
|
val intent = Intent(this, VideoPlayerActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
|
}
|
||||||
|
|
||||||
videoPlayerLauncher.launch(intent)
|
videoPlayerLauncher.launch(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ class VideoPlayerActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
VideoPlayerObject.implementation.pause()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ import nl.jknaapen.fladder.utility.ImmersiveSystemBars
|
||||||
import nl.jknaapen.fladder.utility.defaultSelected
|
import nl.jknaapen.fladder.utility.defaultSelected
|
||||||
import nl.jknaapen.fladder.utility.leanBackEnabled
|
import nl.jknaapen.fladder.utility.leanBackEnabled
|
||||||
import nl.jknaapen.fladder.utility.visible
|
import nl.jknaapen.fladder.utility.visible
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -143,7 +142,8 @@ fun CustomVideoControls(
|
||||||
|
|
||||||
// Restart the multiplier
|
// Restart the multiplier
|
||||||
LaunchedEffect(lastSeekInteraction.longValue) {
|
LaunchedEffect(lastSeekInteraction.longValue) {
|
||||||
delay(2.seconds)
|
delay(1.seconds)
|
||||||
|
if (currentSkipTime == 0L) return@LaunchedEffect
|
||||||
player?.seekTo(position + currentSkipTime)
|
player?.seekTo(position + currentSkipTime)
|
||||||
currentSkipTime = 0L
|
currentSkipTime = 0L
|
||||||
}
|
}
|
||||||
|
|
@ -167,18 +167,12 @@ fun CustomVideoControls(
|
||||||
if (!showControls) {
|
if (!showControls) {
|
||||||
when (keyEvent.key) {
|
when (keyEvent.key) {
|
||||||
DirectionLeft -> {
|
DirectionLeft -> {
|
||||||
if (currentSkipTime == 0L) {
|
|
||||||
player?.seekTo(position - backwardSpeed.inWholeMilliseconds)
|
|
||||||
}
|
|
||||||
currentSkipTime -= backwardSpeed.inWholeMilliseconds
|
currentSkipTime -= backwardSpeed.inWholeMilliseconds
|
||||||
updateSeekInteraction()
|
updateSeekInteraction()
|
||||||
return@onKeyEvent true
|
return@onKeyEvent true
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectionRight -> {
|
DirectionRight -> {
|
||||||
if (currentSkipTime.absoluteValue == 0L) {
|
|
||||||
player?.seekTo(position + forwardSpeed.inWholeMilliseconds)
|
|
||||||
}
|
|
||||||
currentSkipTime += forwardSpeed.inWholeMilliseconds
|
currentSkipTime += forwardSpeed.inWholeMilliseconds
|
||||||
updateSeekInteraction()
|
updateSeekInteraction()
|
||||||
return@onKeyEvent true
|
return@onKeyEvent true
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||||
import nl.jknaapen.fladder.utility.clearAudioTrack
|
import nl.jknaapen.fladder.utility.clearAudioTrack
|
||||||
import nl.jknaapen.fladder.utility.conditional
|
|
||||||
import nl.jknaapen.fladder.utility.setInternalAudioTrack
|
import nl.jknaapen.fladder.utility.setInternalAudioTrack
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
|
|
@ -32,15 +31,31 @@ fun AudioPicker(
|
||||||
val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf())
|
val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf())
|
||||||
val internalAudioTracks by VideoPlayerObject.exoAudioTracks
|
val internalAudioTracks by VideoPlayerObject.exoAudioTracks
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusOffTrack = remember { FocusRequester() }
|
||||||
|
val focusRequesters = remember(internalAudioTracks) {
|
||||||
|
internalAudioTracks.associateWith { FocusRequester() }
|
||||||
|
}
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
LaunchedEffect(selectedIndex) {
|
LaunchedEffect(selectedIndex, audioTracks, internalAudioTracks) {
|
||||||
if (selectedIndex == -1) return@LaunchedEffect
|
if (selectedIndex == -1) {
|
||||||
listState.scrollToItem(
|
focusOffTrack.requestFocus()
|
||||||
audioTracks.indexOfFirst { it.index == selectedIndex.toLong() }
|
return@LaunchedEffect
|
||||||
)
|
}
|
||||||
|
|
||||||
|
val serverTrackIndex = audioTracks.indexOfFirst { it.index == selectedIndex.toLong() }
|
||||||
|
|
||||||
|
if (serverTrackIndex <= 0) {
|
||||||
|
focusOffTrack.requestFocus()
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
val internalIndex = serverTrackIndex - 1
|
||||||
|
val lazyColumnIndex = internalIndex + 1
|
||||||
|
|
||||||
|
listState.scrollToItem(lazyColumnIndex)
|
||||||
|
focusRequesters[internalAudioTracks[internalIndex]]?.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomModalBottomSheet(
|
CustomModalBottomSheet(
|
||||||
|
|
@ -54,48 +69,40 @@ fun AudioPicker(
|
||||||
.padding(horizontal = 8.dp, vertical = 16.dp),
|
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
val selectedOff = -1 == selectedIndex
|
val selectedOff = selectedIndex == -1
|
||||||
TrackButton(
|
TrackButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.conditional(selectedOff) {
|
.focusRequester(focusOffTrack),
|
||||||
focusRequester(focusRequester)
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
VideoPlayerObject.setAudioTrackIndex(-1)
|
VideoPlayerObject.setAudioTrackIndex(-1)
|
||||||
player.clearAudioTrack()
|
player.clearAudioTrack()
|
||||||
},
|
},
|
||||||
selected = selectedOff
|
selected = selectedOff
|
||||||
) {
|
) {
|
||||||
Text(
|
Text("Off")
|
||||||
text = "Off",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internalAudioTracks.forEachIndexed { index, track ->
|
internalAudioTracks.forEachIndexed { index, track ->
|
||||||
val serverTrack = audioTracks.elementAtOrNull(index + 1)
|
val serverTrack = audioTracks.elementAtOrNull(index + 1)
|
||||||
val selected = serverTrack?.index == selectedIndex.toLong()
|
val selected = serverTrack?.index?.toInt() == selectedIndex
|
||||||
|
|
||||||
item {
|
item {
|
||||||
TrackButton(
|
TrackButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.conditional(selected) {
|
.focusRequester(focusRequesters[track]!!),
|
||||||
focusRequester(focusRequester)
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
serverTrack?.index?.let {
|
serverTrack?.index?.let { VideoPlayerObject.setAudioTrackIndex(it.toInt()) }
|
||||||
VideoPlayerObject.setAudioTrackIndex(it.toInt())
|
|
||||||
}
|
|
||||||
player.setInternalAudioTrack(track)
|
player.setInternalAudioTrack(track)
|
||||||
},
|
},
|
||||||
selected = selected
|
selected = selected
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(serverTrack?.name ?: "")
|
||||||
text = serverTrack?.name ?: "",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,20 @@ package nl.jknaapen.fladder.composables.dialogs
|
||||||
|
|
||||||
import Chapter
|
import Chapter
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
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.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -22,20 +24,18 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||||
import nl.jknaapen.fladder.utility.conditional
|
import nl.jknaapen.fladder.utility.highlightOnFocus
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -47,27 +47,23 @@ internal fun ChapterSelectionSheet(
|
||||||
val chapters = playbackData?.chapters ?: listOf()
|
val chapters = playbackData?.chapters ?: listOf()
|
||||||
val currentPosition by VideoPlayerObject.position.collectAsState(0L)
|
val currentPosition by VideoPlayerObject.position.collectAsState(0L)
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequesters = remember(chapters) {
|
||||||
|
chapters.associateWith { FocusRequester() }
|
||||||
|
}
|
||||||
|
|
||||||
if (chapters.isEmpty()) return
|
if (chapters.isEmpty()) return
|
||||||
|
|
||||||
var currentChapter: Chapter? by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
chapters[chapters.indexOfCurrent(
|
|
||||||
currentPosition
|
|
||||||
)]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
LaunchedEffect(chapters, currentPosition) {
|
val currentChapterIndex = remember(currentPosition) {
|
||||||
val chapter = chapters.indexOfCurrent(currentPosition)
|
chapters.indexOfCurrent(currentPosition)
|
||||||
lazyListState.animateScrollToItem(
|
}
|
||||||
chapter
|
|
||||||
)
|
val currentChapter = chapters.getOrNull(currentChapterIndex)
|
||||||
currentChapter = chapters[chapter]
|
|
||||||
focusRequester.requestFocus()
|
LaunchedEffect(currentChapter) {
|
||||||
|
lazyListState.animateScrollToItem(chapters.indexOf(currentChapter))
|
||||||
|
focusRequesters[currentChapter]?.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomModalBottomSheet(
|
CustomModalBottomSheet(
|
||||||
|
|
@ -76,6 +72,8 @@ internal fun ChapterSelectionSheet(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.55f)
|
||||||
|
.wrapContentHeight()
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
.wrapContentHeight(),
|
.wrapContentHeight(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
|
@ -90,36 +88,11 @@ internal fun ChapterSelectionSheet(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
chapters.forEachIndexed { index, chapter ->
|
chapters.forEachIndexed { index, chapter ->
|
||||||
val selectedChapter = currentChapter == chapter
|
|
||||||
val isCurrentChapter = chapters.indexOfCurrent(currentPosition) == index
|
val isCurrentChapter = chapters.indexOfCurrent(currentPosition) == index
|
||||||
item {
|
item {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
|
||||||
color = if (selectedChapter) Color.White.copy(alpha = 0.25f) else Color.Black.copy(
|
|
||||||
alpha = 0.75f
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.aspectRatio(1.67f)
|
|
||||||
.border(
|
|
||||||
width = 2.dp,
|
|
||||||
color = Color.White.copy(alpha = if (selectedChapter) 0.45f else 0f),
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.conditional(selectedChapter) {
|
|
||||||
focusRequester(focusRequester)
|
|
||||||
}
|
|
||||||
.onFocusChanged {
|
|
||||||
if (it.isFocused) {
|
|
||||||
currentChapter = chapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
onClick = {
|
|
||||||
onSelected(chapter)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
|
@ -127,33 +100,48 @@ internal fun ChapterSelectionSheet(
|
||||||
alignment = Alignment.CenterVertically
|
alignment = Alignment.CenterVertically
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = chapter.url,
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequesters[chapter]!!)
|
||||||
|
.highlightOnFocus(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
width = 3.dp,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
onSelected(chapter)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.aspectRatio(1.67f)
|
||||||
|
.clip(shape = RoundedCornerShape(24.dp))
|
||||||
|
.weight(1f),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.FillBounds
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
8.dp,
|
8.dp,
|
||||||
alignment = Alignment.CenterHorizontally
|
alignment = Alignment.Start
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
if (isCurrentChapter)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
chapter.name,
|
chapter.name,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = Color.White
|
color = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AsyncImage(
|
|
||||||
model = chapter.url,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(
|
|
||||||
shape = RoundedCornerShape(24.dp)
|
|
||||||
)
|
|
||||||
.heightIn(min = 125.dp, max = 150.dp)
|
|
||||||
.border(
|
|
||||||
width = 2.dp,
|
|
||||||
color = Color.White.copy(alpha = if (isCurrentChapter) 1f else 0f),
|
|
||||||
shape = RoundedCornerShape(24.dp)
|
|
||||||
),
|
|
||||||
contentDescription = ""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,9 +151,13 @@ internal fun ChapterSelectionSheet(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Chapter>.indexOfCurrent(currentPosition: Long): Int {
|
private fun List<Chapter>.indexOfCurrent(currentPosition: Long): Int {
|
||||||
return this.indexOfFirst { chapter ->
|
if (isEmpty()) return 0
|
||||||
val nextChapterTime =
|
|
||||||
this.getOrNull(this.indexOf(chapter) + 1)?.time ?: Long.MAX_VALUE
|
for (i in indices) {
|
||||||
currentPosition >= chapter.time && currentPosition < nextChapterTime
|
val chapter = this[i]
|
||||||
|
val nextTime = getOrNull(i + 1)?.time ?: Long.MAX_VALUE
|
||||||
|
if (currentPosition in chapter.time until nextTime) return i
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return if (currentPosition < first().time) 0 else lastIndex
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -29,7 +29,11 @@ internal fun CustomModalBottomSheet(
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val modalBottomSheetState = rememberModalBottomSheetState(
|
val modalBottomSheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true,
|
||||||
|
confirmValueChange = { newValue ->
|
||||||
|
newValue == SheetValue.Expanded ||
|
||||||
|
newValue == SheetValue.Hidden
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -47,7 +51,6 @@ internal fun CustomModalBottomSheet(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
|
||||||
.displayCutoutPadding()
|
.displayCutoutPadding()
|
||||||
.background(
|
.background(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||||
import nl.jknaapen.fladder.utility.clearSubtitleTrack
|
import nl.jknaapen.fladder.utility.clearSubtitleTrack
|
||||||
import nl.jknaapen.fladder.utility.conditional
|
|
||||||
import nl.jknaapen.fladder.utility.setInternalSubtitleTrack
|
import nl.jknaapen.fladder.utility.setInternalSubtitleTrack
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
|
|
@ -33,16 +32,27 @@ fun SubtitlePicker(
|
||||||
val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf())
|
val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf())
|
||||||
val internalSubTracks by VideoPlayerObject.exoSubTracks
|
val internalSubTracks by VideoPlayerObject.exoSubTracks
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusOffTrack = remember { FocusRequester() }
|
||||||
|
|
||||||
|
val focusRequesters = remember(internalSubTracks) {
|
||||||
|
internalSubTracks.associateWith { FocusRequester() }
|
||||||
|
}
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
LaunchedEffect(selectedIndex, subTitles) {
|
LaunchedEffect(selectedIndex, subTitles, internalSubTracks) {
|
||||||
if (selectedIndex == -1) return@LaunchedEffect
|
val serverSubIndex = subTitles.indexOfFirst { it.index == selectedIndex.toLong() }
|
||||||
listState.scrollToItem(
|
|
||||||
subTitles.indexOfFirst { it.index == selectedIndex.toLong() }
|
if (serverSubIndex <= 0) {
|
||||||
)
|
focusOffTrack.requestFocus()
|
||||||
focusRequester.requestFocus()
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
val internalIndex = serverSubIndex - 1
|
||||||
|
val lazyColumnIndex = internalIndex + 1
|
||||||
|
|
||||||
|
listState.scrollToItem(lazyColumnIndex)
|
||||||
|
focusRequesters[internalSubTracks[internalIndex]]?.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomModalBottomSheet(
|
CustomModalBottomSheet(
|
||||||
|
|
@ -60,9 +70,7 @@ fun SubtitlePicker(
|
||||||
TrackButton(
|
TrackButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.conditional(selectedOff) {
|
.focusRequester(focusOffTrack),
|
||||||
focusRequester(focusRequester)
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
VideoPlayerObject.setSubtitleTrackIndex(-1)
|
VideoPlayerObject.setSubtitleTrackIndex(-1)
|
||||||
player.clearSubtitleTrack()
|
player.clearSubtitleTrack()
|
||||||
|
|
@ -81,9 +89,7 @@ fun SubtitlePicker(
|
||||||
TrackButton(
|
TrackButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.conditional(selected) {
|
.focusRequester(focusRequesters[subtitle]!!),
|
||||||
focusRequester(focusRequester)
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
serverSub?.index?.let {
|
serverSub?.index?.let {
|
||||||
VideoPlayerObject.setSubtitleTrackIndex(it.toInt())
|
VideoPlayerObject.setSubtitleTrackIndex(it.toInt())
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ 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.CustomButton
|
import nl.jknaapen.fladder.composables.controls.CustomButton
|
||||||
import nl.jknaapen.fladder.utility.defaultSelected
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TrackButton(
|
internal fun TrackButton(
|
||||||
|
|
@ -33,8 +32,7 @@ internal fun TrackButton(
|
||||||
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)
|
||||||
.defaultMinSize(minHeight = 40.dp)
|
.defaultMinSize(minHeight = 40.dp),
|
||||||
.defaultSelected(selected),
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -165,22 +165,29 @@ internal fun NextUpOverlay(
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
"Next-up in $timeUntilNextVideo seconds",
|
modifier = Modifier.weight(1f),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
) {
|
||||||
)
|
Text(
|
||||||
Box(
|
"Next-up in $timeUntilNextVideo seconds",
|
||||||
modifier = Modifier
|
style = MaterialTheme.typography.titleLarge,
|
||||||
.align(alignment = Alignment.CenterHorizontally)
|
maxLines = 1,
|
||||||
.fillMaxWidth(fraction = 0.1f)
|
overflow = TextOverflow.Ellipsis,
|
||||||
.heightIn(2.dp)
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
.background(
|
)
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
Box(
|
||||||
shape = RoundedCornerShape(16.dp),
|
modifier = Modifier
|
||||||
)
|
.align(alignment = Alignment.CenterHorizontally)
|
||||||
)
|
.fillMaxWidth(fraction = 0.1f)
|
||||||
MediaInfo()
|
.heightIn(2.dp)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
MediaInfo()
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ class VideoPlayerImplementation(
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
||||||
player?.stop()
|
player?.stop()
|
||||||
player?.clearMediaItems()
|
player?.clearMediaItems()
|
||||||
player?.setMediaItem(mediaItem)
|
player?.setMediaItem(mediaItem)
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,6 @@ internal fun ExoPlayer(
|
||||||
val videoHost = VideoPlayerObject
|
val videoHost = VideoPlayerObject
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var initialized = false
|
|
||||||
|
|
||||||
val extractorsFactory = DefaultExtractorsFactory().apply {
|
val extractorsFactory = DefaultExtractorsFactory().apply {
|
||||||
val isLowRamDevice = context.getSystemService<ActivityManager>()?.isLowRamDevice == true
|
val isLowRamDevice = context.getSystemService<ActivityManager>()?.isLowRamDevice == true
|
||||||
setTsExtractorTimestampSearchBytes(
|
setTsExtractorTimestampSearchBytes(
|
||||||
|
|
@ -146,7 +144,7 @@ internal fun ExoPlayer(
|
||||||
it.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
it.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
videoHost.setPlaybackState(
|
videoHost.setPlaybackState(
|
||||||
PlaybackState(
|
PlaybackState(
|
||||||
position = exoPlayer.currentPosition,
|
position = exoPlayer.currentPosition,
|
||||||
|
|
@ -167,13 +165,17 @@ internal fun ExoPlayer(
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
super.onTracksChanged(tracks)
|
super.onTracksChanged(tracks)
|
||||||
if (!initialized) {
|
val subTracks = exoPlayer.getSubtitleTracks()
|
||||||
initialized = true
|
val audioTracks = exoPlayer.getAudioTracks()
|
||||||
|
|
||||||
|
if (subTracks.isEmpty() && audioTracks.isEmpty()) return
|
||||||
|
|
||||||
|
if (subTracks != VideoPlayerObject.exoSubTracks.value || audioTracks != VideoPlayerObject.exoAudioTracks.value) {
|
||||||
VideoPlayerObject.implementation.playbackData.value?.let {
|
VideoPlayerObject.implementation.playbackData.value?.let {
|
||||||
exoPlayer.properlySetSubAndAudioTracks(it)
|
exoPlayer.properlySetSubAndAudioTracks(it)
|
||||||
}
|
}
|
||||||
VideoPlayerObject.exoSubTracks.value = exoPlayer.getSubtitleTracks()
|
VideoPlayerObject.exoSubTracks.value = subTracks
|
||||||
VideoPlayerObject.exoAudioTracks.value = exoPlayer.getAudioTracks()
|
VideoPlayerObject.exoAudioTracks.value = audioTracks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package nl.jknaapen.fladder.utility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -13,13 +14,13 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ fun Modifier.highlightOnFocus(
|
||||||
)
|
)
|
||||||
.border(
|
.border(
|
||||||
width = width,
|
width = width,
|
||||||
color = color.copy(alpha = 0.5f),
|
color = color.copy(alpha = 0.8f),
|
||||||
)
|
)
|
||||||
} else
|
} else
|
||||||
if (width != 0.dp) {
|
if (width != 0.dp) {
|
||||||
|
|
@ -54,7 +55,7 @@ fun Modifier.highlightOnFocus(
|
||||||
)
|
)
|
||||||
.border(
|
.border(
|
||||||
width = width,
|
width = width,
|
||||||
color = color.copy(alpha = 0.5f),
|
color = color.copy(alpha = 0.8f),
|
||||||
shape = shape
|
shape = shape
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -104,12 +105,23 @@ fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier):
|
||||||
fun Modifier.visible(
|
fun Modifier.visible(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
): Modifier {
|
): Modifier {
|
||||||
val alphaAnimated by animateFloatAsState(if (visible) 1f else 0f)
|
val alphaAnimated by animateFloatAsState(
|
||||||
|
targetValue = if (visible) 1f else 0f,
|
||||||
|
label = "AlphaAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
alpha = alphaAnimated
|
alpha = alphaAnimated
|
||||||
}
|
}
|
||||||
.then(
|
.then(
|
||||||
if (!visible) Modifier.pointerInput(Unit) {} else Modifier
|
if (!visible) {
|
||||||
|
//Collapse composable to disable input blocking
|
||||||
|
Modifier
|
||||||
|
.size(0.dp)
|
||||||
|
.clipToBounds()
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ fun ExoPlayer.setInternalAudioTrack(audioTrack: InternalTrack) {
|
||||||
selector.setParameters(
|
selector.setParameters(
|
||||||
selector.buildUponParameters()
|
selector.buildUponParameters()
|
||||||
.setRendererDisabled(audioTrack.rendererIndex, false)
|
.setRendererDisabled(audioTrack.rendererIndex, false)
|
||||||
|
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -74,8 +75,12 @@ fun ExoPlayer.clearAudioTrack(disable: Boolean = true) {
|
||||||
selector.setParameters(
|
selector.setParameters(
|
||||||
selector.buildUponParameters()
|
selector.buildUponParameters()
|
||||||
.setRendererDisabled(C.TRACK_TYPE_AUDIO, disable)
|
.setRendererDisabled(C.TRACK_TYPE_AUDIO, disable)
|
||||||
|
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, disable)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.trackSelectionParameters = selector.parameters.buildUpon()
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
|
|
@ -110,21 +115,27 @@ fun ExoPlayer.getSubtitleTracks(): List<InternalTrack> {
|
||||||
fun ExoPlayer.clearSubtitleTrack() {
|
fun ExoPlayer.clearSubtitleTrack() {
|
||||||
val selector = trackSelector as? DefaultTrackSelector ?: return
|
val selector = trackSelector as? DefaultTrackSelector ?: return
|
||||||
val newParams = selector.buildUponParameters()
|
val newParams = selector.buildUponParameters()
|
||||||
.setRendererDisabled(C.TRACK_TYPE_TEXT, false) // keep text renderer active
|
.setRendererDisabled(C.TRACK_TYPE_TEXT, false)
|
||||||
.setPreferredTextLanguage(null) // don't auto-pick a language
|
.setPreferredTextLanguage(null)
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true) // <– disables selection of *any* text track
|
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||||
.build()
|
.build()
|
||||||
selector.setParameters(newParams)
|
selector.setParameters(newParams)
|
||||||
|
|
||||||
|
this.trackSelectionParameters = selector.parameters.buildUpon()
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun ExoPlayer.enableSubtitles(language: String? = null) {
|
fun ExoPlayer.enableSubtitles(language: String? = null) {
|
||||||
val selector = trackSelector as? DefaultTrackSelector ?: return
|
val selector = trackSelector as? DefaultTrackSelector ?: return
|
||||||
val newParams = selector.buildUponParameters()
|
val newParams = selector.buildUponParameters()
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false) // allow text again
|
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||||
.setPreferredTextLanguage(language) // optional: auto-pick by language
|
.setPreferredTextLanguage(language)
|
||||||
.build()
|
.build()
|
||||||
selector.setParameters(newParams)
|
selector.setParameters(newParams)
|
||||||
|
|
||||||
|
this.trackSelectionParameters = selector.parameters.buildUpon()
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue