diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt index 5151e39..17fd901 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt @@ -27,7 +27,7 @@ internal fun extractSeasonEpisode(videoId: String?): Pair? { internal fun PlayerUIContent( content: PlayerContentUiModel, lang: String, duration: Long, position: Long, bufferedFraction: Float, chapters: List = emptyList(), isPlaying: Boolean, isBuffering: Boolean, hasStartedPlaying: Boolean, deviceType: DeviceType, onPlayPause: () -> Unit, onSeek: (Long) -> Unit, onToggleSubtitles: () -> Unit, onToggleAspect: () -> Unit, onSpeedChange: (Float) -> Unit, playbackSpeed: Float, playPauseFocusRequester: FocusRequester, seekbarFocusRequester: FocusRequester, - isScrubbing: Boolean, scrubPosition: Long, onScrubbingChange: (Boolean, Long) -> Unit, onScrubSeek: (Long) -> Unit = {}, + isScrubbing: Boolean, scrubPosition: Long, onScrubbingChange: (Boolean, Long) -> Unit, isSwitchingAudioSource: Boolean = false, detailedStatus: String = "", episodeMetaLine: String? = null, streamDetailLine: String? = null, subtitlesEnabled: Boolean = false, technicalInfo: String? = null, supportsTrackSettings: Boolean = true, seekForwardMs: Long = 10_000L, seekBackwardMs: Long = 10_000L, @@ -46,7 +46,7 @@ internal fun PlayerUIContent( accentColor: Color = FluxaColors.accent ) { val seekPreviewBitmap = rememberSeekThumbnail( - LocalSeekSurfaceView.current, scrubPosition, isScrubbing, position, isPlaying, onScrubSeek + LocalSeekSurfaceView.current, scrubPosition, isScrubbing, position, isPlaying ) if (deviceType == DeviceType.Mobile) { diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSurface.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSurface.kt index 871799e..f3957b3 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSurface.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSurface.kt @@ -303,7 +303,6 @@ internal fun BoxScope.PlayerPlaybackSurface( isScrubbing = isScrubbing, scrubPosition = scrubPosition, onScrubbingChange = onScrubbingChange, - onScrubSeek = { activeEngine?.seekTo(it, exact = false) }, isSwitchingAudioSource = isSwitchingAudioSource, detailedStatus = torrentStatus.detailedStatus, episodeMetaLine = currentEpisodeMetaLine, diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/player/MobileSeekThumbnail.kt b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/player/MobileSeekThumbnail.kt index d254289..fa71448 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/player/MobileSeekThumbnail.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/player/MobileSeekThumbnail.kt @@ -18,11 +18,9 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import kotlinx.coroutines.delay import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull import java.io.ByteArrayOutputStream import kotlin.coroutines.resume @@ -34,7 +32,6 @@ private const val BackgroundCaptureIntervalMs = 60_000L private const val ThumbWidth = 480 private const val ThumbHeight = 270 private const val CacheMaxBytes = 32 * 1024 * 1024 -private const val FrameTimeoutMs = 300L @Composable internal fun rememberSeekThumbnail( @@ -42,45 +39,19 @@ internal fun rememberSeekThumbnail( scrubPosition: Long, isScrubbing: Boolean, livePosition: Long, - isPlaying: Boolean, - onScrubSeek: (Long) -> Unit + isPlaying: Boolean ): ImageBitmap? { - val exoPlayer = LocalSeekExoPlayer.current var thumbnail by remember { mutableStateOf(null) } val cache = remember { object : LruCache(CacheMaxBytes) { override fun sizeOf(key: Long, value: ByteArray) = value.size } } - val latestScrubPosition by rememberUpdatedState(scrubPosition) val bucket = (scrubPosition / BucketMs) * BucketMs - LaunchedEffect(bucket, isScrubbing, surfaceView != null) { + LaunchedEffect(bucket, isScrubbing) { if (!isScrubbing) { thumbnail = null; return@LaunchedEffect } - val sv = surfaceView ?: return@LaunchedEffect - if (android.os.Build.VERSION.SDK_INT < 26) return@LaunchedEffect - - cache[bucket]?.let { thumbnail = decodeJpeg(it); return@LaunchedEffect } - - delay(80) - if (!isScrubbing) return@LaunchedEffect - val target = latestScrubPosition - - if (exoPlayer != null) { - val listener = FirstFrameListener(exoPlayer) - onScrubSeek(target) - withTimeoutOrNull(FrameTimeoutMs) { listener.await() } - listener.detach() - } else { - onScrubSeek(target) - delay(140) - } - if (!isScrubbing) return@LaunchedEffect - - val captured = capturePixelCopy(sv) ?: return@LaunchedEffect - val bytes = compressToJpeg(captured) - cache.put(bucket, bytes) - thumbnail = decodeJpeg(bytes) + cache[bucket]?.let { thumbnail = decodeJpeg(it) } } val latestIsScrubbing by rememberUpdatedState(isScrubbing) @@ -108,29 +79,6 @@ internal fun rememberSeekThumbnail( return thumbnail } -private class FirstFrameListener(private val player: ExoPlayer) : Player.Listener { - private var continuation: kotlinx.coroutines.CancellableContinuation? = null - - init { player.addListener(this) } - - override fun onRenderedFirstFrame() { - val cont = continuation ?: return - continuation = null - player.removeListener(this) - cont.resume(Unit) - } - - suspend fun await() = suspendCancellableCoroutine { cont -> - continuation = cont - cont.invokeOnCancellation { continuation = null } - } - - fun detach() { - continuation = null - runCatching { player.removeListener(this) } - } -} - private fun compressToJpeg(captured: Bitmap): ByteArray { val composed = Bitmap.createBitmap(ThumbWidth, ThumbHeight, Bitmap.Config.ARGB_8888) val canvas = android.graphics.Canvas(composed) diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt index 204f486..314349e 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt @@ -14,8 +14,6 @@ import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsDraggedAsState -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -88,7 +86,9 @@ fun UniversalSettingsSidebar( val twoColumn = deviceType != DeviceType.Mobile || maxWidth > maxHeight var tab by remember(activeTab) { mutableStateOf(activeTab.coerceIn(0, 1)) } var showAdjust by remember(activeTab) { mutableStateOf(false) } - var sliderActive by remember { mutableStateOf(false) } + var liveTextOpacity by remember(subtitleTextOpacity) { mutableStateOf(subtitleTextOpacity) } + var liveBackgroundOpacity by remember(subtitleBackgroundOpacity) { mutableStateOf(subtitleBackgroundOpacity) } + var liveOutlineOpacity by remember(subtitleOutlineOpacity) { mutableStateOf(subtitleOutlineOpacity) } val listMaxHeight = if (deviceType == DeviceType.TV) 400.dp else 440.dp val audioList: @Composable () -> Unit = { @@ -151,24 +151,24 @@ fun UniversalSettingsSidebar( ) OpacityAdjustmentItem( title = AppStrings.t(lang, "settings.subtitle_text"), - value = subtitleTextOpacity, + value = liveTextOpacity, deviceType = deviceType, - onChange = onSubtitleTextOpacityChange, - onDragActiveChange = { sliderActive = it } + onChange = { liveTextOpacity = it }, + onCommit = { onSubtitleTextOpacityChange(liveTextOpacity) } ) OpacityAdjustmentItem( title = AppStrings.t(lang, "settings.subtitle_background"), - value = subtitleBackgroundOpacity, + value = liveBackgroundOpacity, deviceType = deviceType, - onChange = onSubtitleBackgroundOpacityChange, - onDragActiveChange = { sliderActive = it } + onChange = { liveBackgroundOpacity = it }, + onCommit = { onSubtitleBackgroundOpacityChange(liveBackgroundOpacity) } ) OpacityAdjustmentItem( title = AppStrings.t(lang, "settings.subtitle_outline"), - value = subtitleOutlineOpacity, + value = liveOutlineOpacity, deviceType = deviceType, - onChange = onSubtitleOutlineOpacityChange, - onDragActiveChange = { sliderActive = it } + onChange = { liveOutlineOpacity = it }, + onCommit = { onSubtitleOutlineOpacityChange(liveOutlineOpacity) } ) } } @@ -176,9 +176,9 @@ fun UniversalSettingsSidebar( if (showAdjust) { SubtitlePreviewCue( lang = lang, - textOpacity = subtitleTextOpacity, - backgroundOpacity = subtitleBackgroundOpacity, - outlineOpacity = subtitleOutlineOpacity + textOpacity = liveTextOpacity, + backgroundOpacity = liveBackgroundOpacity, + outlineOpacity = liveOutlineOpacity ) PlayerSidebarShell( title = AppStrings.t(lang, "player.adjust"), @@ -187,8 +187,7 @@ fun UniversalSettingsSidebar( onBack = { showAdjust = false }, cardWidth = 380.dp, anchorTop = true, - scrimAlpha = 0.06f, - dimmed = sliderActive + scrimAlpha = 0.06f ) { subtitleAdjust() } @@ -415,7 +414,7 @@ private fun SubtitlePreviewCue( Box( modifier = Modifier .align(Alignment.BottomCenter) - .padding(bottom = 56.dp) + .padding(bottom = 20.dp) .clip(RoundedCornerShape(6.dp)) .background(Color.Black.copy(alpha = backgroundOpacity.coerceIn(0f, 1f))) .padding(horizontal = 12.dp, vertical = 6.dp) @@ -488,12 +487,8 @@ private fun OpacityAdjustmentItem( value: Float, deviceType: DeviceType, onChange: (Float) -> Unit, - onDragActiveChange: (Boolean) -> Unit = {} + onCommit: () -> Unit = {} ) { - val sliderInteraction = remember { MutableInteractionSource() } - val dragged by sliderInteraction.collectIsDraggedAsState() - val pressed by sliderInteraction.collectIsPressedAsState() - LaunchedEffect(dragged, pressed) { onDragActiveChange(dragged || pressed) } Row( modifier = Modifier .fillMaxWidth() @@ -515,8 +510,8 @@ private fun OpacityAdjustmentItem( Slider( value = value.coerceIn(0f, 1f), onValueChange = { onChange(it.coerceIn(0f, 1f)) }, + onValueChangeFinished = onCommit, valueRange = 0f..1f, - interactionSource = sliderInteraction, colors = SliderDefaults.colors( thumbColor = Color.White, activeTrackColor = Color.White, diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt index 2363535..8679742 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt @@ -81,7 +81,6 @@ fun PlayerSidebarShell( cardWidth: Dp? = null, anchorTop: Boolean = false, scrimAlpha: Float = 0.42f, - dimmed: Boolean = false, content: @Composable ColumnScope.() -> Unit ) { var shown by remember { mutableStateOf(false) } @@ -104,12 +103,6 @@ fun PlayerSidebarShell( } val isMobile = deviceType == DeviceType.Mobile - val dim by animateFloatAsState( - targetValue = if (dimmed) 0.15f else 1f, - animationSpec = tween(150), - label = "sidebarDim" - ) - BoxWithConstraints( modifier = Modifier .fillMaxSize() @@ -124,7 +117,7 @@ fun PlayerSidebarShell( Box( modifier = Modifier .fillMaxSize() - .background(Color.Black.copy(alpha = scrimAlpha * progress * dim)) + .background(Color.Black.copy(alpha = scrimAlpha * progress)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null @@ -166,7 +159,7 @@ fun PlayerSidebarShell( .then(panelSizeModifier) .animateContentSize(tween(220, easing = FastOutSlowInEasing)) .graphicsLayer { - alpha = (if (isCard) progress else 1f) * dim + alpha = if (isCard) progress else 1f translationY = if (isCard) { (1f - progress) * (if (anchorTop) (-16).dp.toPx() else 16.dp.toPx()) } else {