Fix shared UI boundary violations from the CMP player overlay migration

checkSharedUiBoundary was failing on master because the July 17 move of
player overlays into shared/commonMain kept imports of data/player domain
types (Meta, Stream, Video, IntroTimestamps, UserProfile, Chapter,
MediaTrack, TorrentStreamStatus, TrailerCue). Relocate the plain value
types into the shared-safe com.fluxa.app.shared.feature.player package,
and replace the remaining domain-object usages with small UI models
(StreamSourceUiModel, SkipSegmentUiModel, NextEpisodePreviewUiModel)
mapped at the Android call sites. Also drops the dead, UserProfile-only
QuickSettingsSidebar/TrackSidebar composables that had no callers.
This commit is contained in:
KhooLy 2026-07-20 15:37:30 +03:00
parent 5cb1bb75db
commit d5f3819ebe
43 changed files with 131 additions and 107 deletions

View file

@ -81,7 +81,7 @@ class AndroidCatalogHomeDataSource(
val items: List<com.fluxa.app.data.remote.Meta>, val items: List<com.fluxa.app.data.remote.Meta>,
val logoUrl: String?, val logoUrl: String?,
val trailerUrl: String?, val trailerUrl: String?,
val trailerSubtitleCues: List<com.fluxa.app.player.TrailerCue> val trailerSubtitleCues: List<com.fluxa.app.shared.feature.player.TrailerCue>
) )
private fun billboardResolution(): Flow<BillboardResolution> = combine( private fun billboardResolution(): Flow<BillboardResolution> = combine(
@ -109,7 +109,7 @@ class AndroidCatalogHomeDataSource(
billboardPool: List<com.fluxa.app.data.remote.Meta>, billboardPool: List<com.fluxa.app.data.remote.Meta>,
billboardLogo: String?, billboardLogo: String?,
billboardTrailerUrl: String?, billboardTrailerUrl: String?,
billboardTrailerSubtitleCues: List<com.fluxa.app.player.TrailerCue>, billboardTrailerSubtitleCues: List<com.fluxa.app.shared.feature.player.TrailerCue>,
filter: String filter: String
): BillboardResolution { ): BillboardResolution {
val filteredPool = billboardPool.filter { it.matchesFilter(filter) } val filteredPool = billboardPool.filter { it.matchesFilter(filter) }

View file

@ -14,7 +14,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.fluxa.app.player.TrailerCue import com.fluxa.app.shared.feature.player.TrailerCue
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive

View file

@ -1,9 +1,9 @@
package com.fluxa.app.ui.catalog package com.fluxa.app.ui.catalog
import com.fluxa.app.player.TrailerCue import com.fluxa.app.shared.feature.player.TrailerCue
import com.fluxa.app.player.TrailerResolveResult import com.fluxa.app.shared.feature.player.TrailerResolveResult
import com.fluxa.app.player.TrailerResult import com.fluxa.app.shared.feature.player.TrailerResult
import com.fluxa.app.player.TrailerSubtitle import com.fluxa.app.shared.feature.player.TrailerSubtitle
import android.util.LruCache import android.util.LruCache
import com.fluxa.app.core.rust.FluxaCoreNative import com.fluxa.app.core.rust.FluxaCoreNative

View file

@ -1,7 +1,7 @@
package com.fluxa.app.ui.catalog package com.fluxa.app.ui.catalog
import com.fluxa.app.data.remote.Meta import com.fluxa.app.data.remote.Meta
import com.fluxa.app.player.TrailerCue import com.fluxa.app.shared.feature.player.TrailerCue
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow

View file

@ -165,7 +165,7 @@ class HomeViewModel @Inject constructor(
val billboardWatchlist: StateFlow<Boolean> = billboardState.watchlist val billboardWatchlist: StateFlow<Boolean> = billboardState.watchlist
val billboardNextEpisode: StateFlow<String?> = billboardState.nextEpisode val billboardNextEpisode: StateFlow<String?> = billboardState.nextEpisode
val billboardTrailerUrl: StateFlow<String?> = billboardState.trailerUrl val billboardTrailerUrl: StateFlow<String?> = billboardState.trailerUrl
val billboardTrailerSubtitleCues: StateFlow<List<com.fluxa.app.player.TrailerCue>> = billboardState.trailerSubtitleCues val billboardTrailerSubtitleCues: StateFlow<List<com.fluxa.app.shared.feature.player.TrailerCue>> = billboardState.trailerSubtitleCues
val billboardSeasonPosterUrl: StateFlow<String?> = billboardState.seasonPosterUrl val billboardSeasonPosterUrl: StateFlow<String?> = billboardState.seasonPosterUrl
private val _isDirectLoading = MutableStateFlow(false) private val _isDirectLoading = MutableStateFlow(false)

View file

@ -19,7 +19,7 @@ import com.fluxa.app.player.LibassDebugLog
import com.fluxa.app.player.LibassRenderThread import com.fluxa.app.player.LibassRenderThread
import com.fluxa.app.player.LibassVideoFrame import com.fluxa.app.player.LibassVideoFrame
import com.fluxa.app.player.MediaPlayerController import com.fluxa.app.player.MediaPlayerController
import com.fluxa.app.player.MediaTrack import com.fluxa.app.shared.feature.player.MediaTrack
import com.fluxa.app.player.MkvNativeAssExtractor import com.fluxa.app.player.MkvNativeAssExtractor
import com.fluxa.app.player.NativeAssTrack import com.fluxa.app.player.NativeAssTrack
import com.fluxa.app.player.NativeLibassRenderer import com.fluxa.app.player.NativeLibassRenderer

View file

@ -26,7 +26,7 @@ import com.fluxa.app.player.PlayerEngine
import com.fluxa.app.player.PlayerEngineRequest import com.fluxa.app.player.PlayerEngineRequest
import com.fluxa.app.player.TorrentStreamManager import com.fluxa.app.player.TorrentStreamManager
import com.fluxa.app.player.TorrentStreamResult import com.fluxa.app.player.TorrentStreamResult
import com.fluxa.app.player.TorrentStreamStatus import com.fluxa.app.shared.feature.player.TorrentStreamStatus
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View file

@ -29,7 +29,7 @@ internal fun extractSeasonEpisode(videoId: String?): Pair<Int, Int>? {
@Composable @Composable
internal fun PlayerUIContent( internal fun PlayerUIContent(
content: PlayerContentUiModel, lang: String, duration: Long, position: Long, bufferedFraction: Float, chapters: List<com.fluxa.app.player.Chapter> = emptyList(), isPlaying: Boolean, isBuffering: Boolean, hasStartedPlaying: Boolean, deviceType: DeviceType, content: PlayerContentUiModel, lang: String, duration: Long, position: Long, bufferedFraction: Float, chapters: List<com.fluxa.app.shared.feature.player.Chapter> = 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, 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, onScrubSeek: (Long) -> Unit = {},
isSwitchingAudioSource: Boolean = false, detailedStatus: String = "", episodeMetaLine: String? = null, streamDetailLine: String? = null, subtitlesEnabled: Boolean = false, technicalInfo: String? = null, isSwitchingAudioSource: Boolean = false, detailedStatus: String = "", episodeMetaLine: String? = null, streamDetailLine: String? = null, subtitlesEnabled: Boolean = false, technicalInfo: String? = null,

View file

@ -27,7 +27,7 @@ import com.fluxa.app.player.NativeAssTrack
import com.fluxa.app.player.PlayerEngine import com.fluxa.app.player.PlayerEngine
import com.fluxa.app.player.PlayerEngineRequest import com.fluxa.app.player.PlayerEngineRequest
import com.fluxa.app.player.TorrentStreamManager import com.fluxa.app.player.TorrentStreamManager
import com.fluxa.app.player.TorrentStreamStatus import com.fluxa.app.shared.feature.player.TorrentStreamStatus
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -17,7 +17,7 @@ import com.fluxa.app.data.remote.Meta
import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Stream
import com.fluxa.app.data.remote.Video import com.fluxa.app.data.remote.Video
import com.fluxa.app.data.repository.TraktIntegration import com.fluxa.app.data.repository.TraktIntegration
import com.fluxa.app.shared.feature.player.withCurrentEpisodeArtwork import com.fluxa.app.data.remote.withCurrentEpisodeArtwork
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -36,16 +36,14 @@ import androidx.media3.ui.PlayerView
import com.fluxa.app.R import com.fluxa.app.R
import com.fluxa.app.data.local.* import com.fluxa.app.data.local.*
import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.local.UserProfile
import com.fluxa.app.data.remote.IntroTimestamps
import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Stream
import com.fluxa.app.data.remote.Video import com.fluxa.app.shared.feature.player.MediaTrack
import com.fluxa.app.player.MediaTrack
import com.fluxa.app.player.ExternalSubtitleTrack import com.fluxa.app.player.ExternalSubtitleTrack
import com.fluxa.app.player.NativeAssTrack import com.fluxa.app.player.NativeAssTrack
import com.fluxa.app.player.MpvAndroidSurfaceView import com.fluxa.app.player.MpvAndroidSurfaceView
import com.fluxa.app.player.MpvEmbeddedPlayer import com.fluxa.app.player.MpvEmbeddedPlayer
import com.fluxa.app.player.PlayerEngine import com.fluxa.app.player.PlayerEngine
import com.fluxa.app.player.TorrentStreamStatus import com.fluxa.app.shared.feature.player.TorrentStreamStatus
import com.fluxa.app.core.rust.FluxaCoreNative import com.fluxa.app.core.rust.FluxaCoreNative
import com.fluxa.app.shared.feature.player.ArtisticLoadingOverlay import com.fluxa.app.shared.feature.player.ArtisticLoadingOverlay
import com.fluxa.app.shared.feature.player.MarkSegmentSidebar import com.fluxa.app.shared.feature.player.MarkSegmentSidebar
@ -118,7 +116,7 @@ internal fun BoxScope.PlayerPlaybackSurface(
seekBackwardMs: Long, seekBackwardMs: Long,
hasPreviousEpisode: Boolean, hasPreviousEpisode: Boolean,
hasNextEpisode: Boolean, hasNextEpisode: Boolean,
nextEpisode: Video?, nextEpisode: NextEpisodePreviewUiModel?,
onPlayPrevious: () -> Unit, onPlayPrevious: () -> Unit,
onPlayNext: () -> Unit, onPlayNext: () -> Unit,
onCast: () -> Unit, onCast: () -> Unit,
@ -128,11 +126,11 @@ internal fun BoxScope.PlayerPlaybackSurface(
onClose: () -> Unit, onClose: () -> Unit,
onNextEpisodeCardShown: () -> Unit, onNextEpisodeCardShown: () -> Unit,
timelinePosition: () -> Long, timelinePosition: () -> Long,
skipSegments: List<IntroTimestamps>, skipSegments: List<SkipSegmentUiModel>,
chapters: List<com.fluxa.app.player.Chapter> = emptyList(), chapters: List<com.fluxa.app.shared.feature.player.Chapter> = emptyList(),
dismissedSkipSegments: Set<String>, dismissedSkipSegments: Set<String>,
onSkipSegment: (IntroTimestamps) -> Unit, onSkipSegment: (SkipSegmentUiModel) -> Unit,
onDismissSegment: (IntroTimestamps) -> Unit, onDismissSegment: (SkipSegmentUiModel) -> Unit,
showSegmentSkipFeedback: Boolean, showSegmentSkipFeedback: Boolean,
holdSpeedVisible: Boolean, holdSpeedVisible: Boolean,
showVolumeBar: Boolean, showVolumeBar: Boolean,
@ -502,7 +500,7 @@ internal fun PlayerSettingsPanel(
) )
} else if (activeSettingsTab == 4) { } else if (activeSettingsTab == 4) {
SourceSidebar( SourceSidebar(
streams = currentStreams, streams = currentStreams.map { it.toSourceUiModel() },
currentUrl = currentUrl.orEmpty(), currentUrl = currentUrl.orEmpty(),
deviceType = deviceType, deviceType = deviceType,
lang = lang, lang = lang,

View file

@ -25,8 +25,7 @@ import com.fluxa.app.data.remote.Meta
import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Stream
import com.fluxa.app.player.* import com.fluxa.app.player.*
import com.fluxa.app.player.MediaPlayerController import com.fluxa.app.player.MediaPlayerController
import com.fluxa.app.player.MediaTrack import com.fluxa.app.shared.feature.player.MediaTrack
import com.fluxa.app.shared.feature.player.dismissKey
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow

View file

@ -52,12 +52,11 @@ import com.fluxa.app.data.local.*
import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.local.UserProfile
import com.fluxa.app.data.remote.Meta import com.fluxa.app.data.remote.Meta
import com.fluxa.app.data.repository.IntroDbSubmitResult import com.fluxa.app.data.repository.IntroDbSubmitResult
import com.fluxa.app.player.MediaTrack import com.fluxa.app.shared.feature.player.MediaTrack
import com.fluxa.app.player.MpvEmbeddedPlayer import com.fluxa.app.player.MpvEmbeddedPlayer
import com.fluxa.app.player.PlayerEngine import com.fluxa.app.player.PlayerEngine
import com.fluxa.app.player.TorrentStreamStatus import com.fluxa.app.shared.feature.player.TorrentStreamStatus
import com.fluxa.app.shared.feature.player.PlayerTopIconButton import com.fluxa.app.shared.feature.player.PlayerTopIconButton
import com.fluxa.app.shared.feature.player.dismissKey
import com.fluxa.app.shared.feature.player.playerInputControls import com.fluxa.app.shared.feature.player.playerInputControls
import com.fluxa.app.shared.feature.player.playerText import com.fluxa.app.shared.feature.player.playerText
@ -288,7 +287,7 @@ internal fun PlayerScreenContent(
seekBackwardMs = seekBackwardMs, seekBackwardMs = seekBackwardMs,
hasPreviousEpisode = state.previousEpisodePending != null, hasPreviousEpisode = state.previousEpisodePending != null,
hasNextEpisode = state.nextEpisodePending != null, hasNextEpisode = state.nextEpisodePending != null,
nextEpisode = state.nextEpisodePending, nextEpisode = state.nextEpisodePending?.toNextEpisodePreviewUiModel(),
onPlayPrevious = playPrevious, onPlayPrevious = playPrevious,
onPlayNext = playNext, onPlayNext = playNext,
onCast = smartCast, onCast = smartCast,
@ -310,11 +309,11 @@ internal fun PlayerScreenContent(
state.nextEpisodePending?.let { viewModel.onNextEpisodeCardShown(meta, it.id, activeProfile) } state.nextEpisodePending?.let { viewModel.onNextEpisodeCardShown(meta, it.id, activeProfile) }
}, },
timelinePosition = { if (useMpvBackend) state.engine.timeline.position else state.timelinePosition }, timelinePosition = { if (useMpvBackend) state.engine.timeline.position else state.timelinePosition },
skipSegments = state.skipSegments, skipSegments = state.skipSegments.map { it.toSkipSegmentUiModel() },
chapters = state.chapters, chapters = state.chapters,
dismissedSkipSegments = state.dismissedSkipSegments, dismissedSkipSegments = state.dismissedSkipSegments,
onSkipSegment = { segment -> onSkipSegment = { segment ->
if (segment.type == "outro") playNext() else seekSafely(segment.endTime) if (segment.type == "outro") playNext() else seekSafely(segment.endMs)
state.dismissedSkipSegments = state.dismissedSkipSegments + segment.dismissKey() state.dismissedSkipSegments = state.dismissedSkipSegments + segment.dismissKey()
state.introAutoSkipped = true state.introAutoSkipped = true
state.segmentSkipFeedbackVersion += 1 state.segmentSkipFeedbackVersion += 1

View file

@ -79,7 +79,7 @@ import coil3.compose.AsyncImage
import com.fluxa.app.R import com.fluxa.app.R
import com.fluxa.app.player.* import com.fluxa.app.player.*
import com.fluxa.app.player.MediaPlayerController import com.fluxa.app.player.MediaPlayerController
import com.fluxa.app.player.MediaTrack import com.fluxa.app.shared.feature.player.MediaTrack
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll

View file

@ -78,7 +78,7 @@ internal class PlayerScreenState(
var currentEpisodeMetaLine by mutableStateOf<String?>(null) var currentEpisodeMetaLine by mutableStateOf<String?>(null)
var currentEpisodeArtwork by mutableStateOf<String?>(null) var currentEpisodeArtwork by mutableStateOf<String?>(null)
var skipSegments by mutableStateOf<List<IntroTimestamps>>(emptyList()) var skipSegments by mutableStateOf<List<IntroTimestamps>>(emptyList())
var chapters by mutableStateOf<List<com.fluxa.app.player.Chapter>>(emptyList()) var chapters by mutableStateOf<List<com.fluxa.app.shared.feature.player.Chapter>>(emptyList())
var dismissedSkipSegments by mutableStateOf<Set<String>>(emptySet()) var dismissedSkipSegments by mutableStateOf<Set<String>>(emptySet())
var introAutoSkipped by mutableStateOf(false) var introAutoSkipped by mutableStateOf(false)
var preferredBingeGroupForNextEpisode by mutableStateOf<String?>(null) var preferredBingeGroupForNextEpisode by mutableStateOf<String?>(null)

View file

@ -22,7 +22,7 @@ import com.fluxa.app.player.PlayerEngine
import com.fluxa.app.player.PlayerEngineRequest import com.fluxa.app.player.PlayerEngineRequest
import com.fluxa.app.player.TorrentStreamManager import com.fluxa.app.player.TorrentStreamManager
import com.fluxa.app.player.TorrentStreamResult import com.fluxa.app.player.TorrentStreamResult
import com.fluxa.app.player.TorrentStreamStatus import com.fluxa.app.shared.feature.player.TorrentStreamStatus
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View file

@ -10,9 +10,9 @@ import com.fluxa.app.data.local.*
import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.local.UserProfile
import com.fluxa.app.data.remote.Meta import com.fluxa.app.data.remote.Meta
import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Stream
import com.fluxa.app.player.MediaTrack import com.fluxa.app.shared.feature.player.MediaTrack
import com.fluxa.app.player.PlayerEngine import com.fluxa.app.player.PlayerEngine
import com.fluxa.app.shared.feature.player.withCurrentEpisodeArtwork import com.fluxa.app.data.remote.withCurrentEpisodeArtwork
import java.util.Locale import java.util.Locale
@Composable @Composable

View file

@ -7,7 +7,7 @@ import com.fluxa.app.domain.discovery.*
import com.fluxa.app.core.rust.FluxaCoreNative import com.fluxa.app.core.rust.FluxaCoreNative
import com.fluxa.app.core.rust.models.SubtitleTrackRef import com.fluxa.app.core.rust.models.SubtitleTrackRef
import com.fluxa.app.player.MediaTrack import com.fluxa.app.shared.feature.player.MediaTrack
import com.fluxa.app.player.resolveAudioLanguagePreference import com.fluxa.app.player.resolveAudioLanguagePreference
import java.util.Locale import java.util.Locale

View file

@ -1,5 +1,6 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.Chapter
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer

View file

@ -6,7 +6,7 @@ import com.fluxa.app.data.repository.*
import com.fluxa.app.domain.discovery.* import com.fluxa.app.domain.discovery.*
import androidx.media3.common.C import androidx.media3.common.C
import com.fluxa.app.player.MediaTrack import com.fluxa.app.shared.feature.player.MediaTrack
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue

View file

@ -475,3 +475,9 @@ object MetaDetailSerializer : KSerializer<MetaDetail> {
return metaDetailFromJson(json.decodeJsonElement()) return metaDetailFromJson(json.decodeJsonElement())
} }
} }
fun Meta.withCurrentEpisodeArtwork(artwork: String?): Meta {
val episodeArtwork = artwork?.takeIf { it.isNotBlank() } ?: return this
if (type != "series") return this
return copy(continueWatchingPoster = episodeArtwork, continueWatchingBackground = episodeArtwork)
}

View file

@ -0,0 +1,38 @@
package com.fluxa.app.ui.catalog
import com.fluxa.app.data.remote.IntroTimestamps
import com.fluxa.app.data.remote.Video
data class SkipSegmentUiModel(
val type: String,
val startMs: Long,
val endMs: Long
) {
fun dismissKey(): String = "$type:$startMs:$endMs"
}
fun IntroTimestamps.toSkipSegmentUiModel(): SkipSegmentUiModel = SkipSegmentUiModel(
type = type,
startMs = startTime,
endMs = endTime
)
fun IntroTimestamps.dismissKey(): String {
return "$type:$startTime:$endTime"
}
data class NextEpisodePreviewUiModel(
val id: String,
val thumbnail: String?,
val season: Int?,
val number: Int?,
val name: String?
)
fun Video.toNextEpisodePreviewUiModel(): NextEpisodePreviewUiModel = NextEpisodePreviewUiModel(
id = id,
thumbnail = thumbnail,
season = season,
number = number,
name = name
)

View file

@ -2,6 +2,18 @@ package com.fluxa.app.ui.catalog
import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Stream
data class StreamSourceUiModel(
val playableUrl: String?,
val header: String,
val body: String?
)
fun Stream.toSourceUiModel(): StreamSourceUiModel = StreamSourceUiModel(
playableUrl = playableUrl,
header = streamSourceHeader(),
body = streamRawBody()
)
fun Stream.streamSourceHeader(): String { fun Stream.streamSourceHeader(): String {
return name return name
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }

View file

@ -1,5 +1,7 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.MediaTrack
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.media3.common.* import androidx.media3.common.*

View file

@ -1,5 +1,7 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.Chapter
import com.fluxa.app.core.rust.FluxaStreamingNative import com.fluxa.app.core.rust.FluxaStreamingNative
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View file

@ -1,5 +1,8 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.Chapter
import com.fluxa.app.shared.feature.player.MediaTrack
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.media.AudioFormat import android.media.AudioFormat

View file

@ -2,6 +2,9 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.Chapter
import com.fluxa.app.shared.feature.player.MediaTrack
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters import androidx.media3.exoplayer.SeekParameters
import androidx.media3.common.C import androidx.media3.common.C

View file

@ -1,5 +1,7 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.TorrentStreamStatus
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities

View file

@ -1,5 +1,7 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.Chapter
import com.fluxa.app.data.remote.IntroTimestamps import com.fluxa.app.data.remote.IntroTimestamps
private fun classifyChapterSkipType(title: String): String? { private fun classifyChapterSkipType(title: String): String? {

View file

@ -1,4 +1,4 @@
package com.fluxa.app.player package com.fluxa.app.shared.feature.player
data class Chapter( data class Chapter(
val title: String, val title: String,

View file

@ -1,4 +1,4 @@
package com.fluxa.app.player package com.fluxa.app.shared.feature.player
data class MediaTrack( data class MediaTrack(
val id: String, val id: String,

View file

@ -1,4 +1,4 @@
package com.fluxa.app.player package com.fluxa.app.shared.feature.player
data class TorrentStreamStatus( data class TorrentStreamStatus(
val bufferProgress: Int = 0, val bufferProgress: Int = 0,

View file

@ -1,4 +1,4 @@
package com.fluxa.app.player package com.fluxa.app.shared.feature.player
data class TrailerSubtitle( data class TrailerSubtitle(
val languageTag: String, val languageTag: String,

View file

@ -1,5 +1,7 @@
package com.fluxa.app.player package com.fluxa.app.player
import com.fluxa.app.shared.feature.player.TrailerPolicy
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNull import kotlin.test.assertNull

View file

@ -125,7 +125,7 @@ import com.fluxa.app.shared.feature.search.SearchUiState
import com.fluxa.app.shared.feature.player.PlayerControlsSurface import com.fluxa.app.shared.feature.player.PlayerControlsSurface
import com.fluxa.app.shared.feature.player.PlayerRenderAction import com.fluxa.app.shared.feature.player.PlayerRenderAction
import com.fluxa.app.shared.feature.player.PlayerRenderState import com.fluxa.app.shared.feature.player.PlayerRenderState
import com.fluxa.app.player.TrailerCue import com.fluxa.app.shared.feature.player.TrailerCue
import com.fluxa.app.ui.catalog.CatalogCard import com.fluxa.app.ui.catalog.CatalogCard
import com.fluxa.app.ui.catalog.FluxaColors import com.fluxa.app.ui.catalog.FluxaColors
import com.fluxa.app.ui.catalog.PosterActionSheet import com.fluxa.app.ui.catalog.PosterActionSheet

View file

@ -3,6 +3,6 @@ package com.fluxa.app.shared
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.fluxa.app.player.TrailerCue import com.fluxa.app.shared.feature.player.TrailerCue
val LocalHeroTrailerSurface = staticCompositionLocalOf<(@Composable (String, List<TrailerCue>, (String) -> Unit, Modifier) -> Unit)?> { null } val LocalHeroTrailerSurface = staticCompositionLocalOf<(@Composable (String, List<TrailerCue>, (String) -> Unit, Modifier) -> Unit)?> { null }

View file

@ -1,6 +1,6 @@
package com.fluxa.app.shared.feature.catalog package com.fluxa.app.shared.feature.catalog
import com.fluxa.app.player.TrailerCue import com.fluxa.app.shared.feature.player.TrailerCue
import com.fluxa.app.ui.catalog.CatalogCardUiModel import com.fluxa.app.ui.catalog.CatalogCardUiModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View file

@ -2,7 +2,6 @@
package com.fluxa.app.shared.feature.player package com.fluxa.app.shared.feature.player
import com.fluxa.app.common.AppStrings import com.fluxa.app.common.AppStrings
import com.fluxa.app.player.Chapter
import com.fluxa.app.ui.catalog.DeviceType import com.fluxa.app.ui.catalog.DeviceType
import com.fluxa.app.ui.catalog.FluxaColors import com.fluxa.app.ui.catalog.FluxaColors
import com.fluxa.app.ui.catalog.FluxaDimensions import com.fluxa.app.ui.catalog.FluxaDimensions

View file

@ -1,13 +1,12 @@
package com.fluxa.app.shared.feature.player package com.fluxa.app.shared.feature.player
import com.fluxa.app.common.AppStrings import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.remote.Video
import com.fluxa.app.player.TorrentStreamStatus
import com.fluxa.app.ui.catalog.BufferSnapshot import com.fluxa.app.ui.catalog.BufferSnapshot
import com.fluxa.app.ui.catalog.DeviceType import com.fluxa.app.ui.catalog.DeviceType
import com.fluxa.app.ui.catalog.FluxaColors import com.fluxa.app.ui.catalog.FluxaColors
import com.fluxa.app.ui.catalog.FluxaDimensions import com.fluxa.app.ui.catalog.FluxaDimensions
import com.fluxa.app.ui.catalog.FluxaIcons import com.fluxa.app.ui.catalog.FluxaIcons
import com.fluxa.app.ui.catalog.NextEpisodePreviewUiModel
import com.fluxa.app.ui.catalog.PlaybackSnapshot import com.fluxa.app.ui.catalog.PlaybackSnapshot
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
@ -66,7 +65,7 @@ fun playerStatusText(lang: String?, value: String): String {
fun SkipSegmentCard( fun SkipSegmentCard(
deviceType: DeviceType, deviceType: DeviceType,
type: String, type: String,
nextEpisode: Video? = null, nextEpisode: NextEpisodePreviewUiModel? = null,
lang: String? = "en", lang: String? = "en",
autoAdvanceSeconds: Int? = null, autoAdvanceSeconds: Int? = null,
onSkip: () -> Unit, onSkip: () -> Unit,
@ -126,7 +125,7 @@ fun SkipSegmentCard(
@Composable @Composable
private fun NextEpisodeSkipCard( private fun NextEpisodeSkipCard(
deviceType: DeviceType, deviceType: DeviceType,
episode: Video, episode: NextEpisodePreviewUiModel,
lang: String?, lang: String?,
autoAdvanceSeconds: Int? = null, autoAdvanceSeconds: Int? = null,
onSkip: () -> Unit onSkip: () -> Unit
@ -203,7 +202,7 @@ private fun NextEpisodeSkipCard(
} }
} }
private fun nextEpisodeSubtitle(lang: String?, episode: Video): String { private fun nextEpisodeSubtitle(lang: String?, episode: NextEpisodePreviewUiModel): String {
val season = episode.season ?: 1 val season = episode.season ?: 1
val number = episode.number ?: 0 val number = episode.number ?: 0
val name = episode.name.orEmpty().trim() val name = episode.name.orEmpty().trim()

View file

@ -1,10 +1,10 @@
package com.fluxa.app.shared.feature.player package com.fluxa.app.shared.feature.player
import com.fluxa.app.data.remote.IntroTimestamps
import com.fluxa.app.data.remote.Video
import com.fluxa.app.ui.catalog.DeviceType import com.fluxa.app.ui.catalog.DeviceType
import com.fluxa.app.ui.catalog.FluxaDimensions import com.fluxa.app.ui.catalog.FluxaDimensions
import com.fluxa.app.ui.catalog.FluxaIcons import com.fluxa.app.ui.catalog.FluxaIcons
import com.fluxa.app.ui.catalog.NextEpisodePreviewUiModel
import com.fluxa.app.ui.catalog.SkipSegmentUiModel
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
@ -51,26 +51,26 @@ import androidx.compose.ui.zIndex
@Composable @Composable
fun PlayerSkipSegmentOverlay( fun PlayerSkipSegmentOverlay(
currentPosition: Long, currentPosition: Long,
skipSegments: List<IntroTimestamps>, skipSegments: List<SkipSegmentUiModel>,
dismissedSkipSegments: Set<String>, dismissedSkipSegments: Set<String>,
hasStartedPlaying: Boolean, hasStartedPlaying: Boolean,
showControls: Boolean, showControls: Boolean,
deviceType: DeviceType, deviceType: DeviceType,
nextEpisode: Video?, nextEpisode: NextEpisodePreviewUiModel?,
nextEpisodeThresholdReached: Boolean, nextEpisodeThresholdReached: Boolean,
autoSkipSegments: Boolean, autoSkipSegments: Boolean,
autoPlayCountdownSeconds: Int? = null, autoPlayCountdownSeconds: Int? = null,
lang: String, lang: String,
onSkipSegment: (IntroTimestamps) -> Unit, onSkipSegment: (SkipSegmentUiModel) -> Unit,
onPlayNextEpisode: () -> Unit, onPlayNextEpisode: () -> Unit,
onDismissSegment: (IntroTimestamps) -> Unit, onDismissSegment: (SkipSegmentUiModel) -> Unit,
onNextEpisodeCardShown: () -> Unit, onNextEpisodeCardShown: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val activeSkipSegment = remember(currentPosition, skipSegments, dismissedSkipSegments, hasStartedPlaying, autoSkipSegments) { val activeSkipSegment = remember(currentPosition, skipSegments, dismissedSkipSegments, hasStartedPlaying, autoSkipSegments) {
if (!hasStartedPlaying || autoSkipSegments) null if (!hasStartedPlaying || autoSkipSegments) null
else skipSegments.find { segment -> else skipSegments.find { segment ->
currentPosition in segment.startTime until segment.endTime && currentPosition in segment.startMs until segment.endMs &&
segment.dismissKey() !in dismissedSkipSegments segment.dismissKey() !in dismissedSkipSegments
} }
} }
@ -307,7 +307,3 @@ fun BoxScope.PlayerTransientOverlays(
} }
} }
} }
fun IntroTimestamps.dismissKey(): String {
return "$type:$startTime:$endTime"
}

View file

@ -1,7 +1,6 @@
package com.fluxa.app.shared.feature.player package com.fluxa.app.shared.feature.player
import com.fluxa.app.common.AppStrings import com.fluxa.app.common.AppStrings
import com.fluxa.app.player.MediaTrack
import com.fluxa.app.ui.catalog.DeviceType import com.fluxa.app.ui.catalog.DeviceType
import com.fluxa.app.ui.catalog.FluxaIcons import com.fluxa.app.ui.catalog.FluxaIcons

View file

@ -1,13 +1,10 @@
package com.fluxa.app.shared.feature.player package com.fluxa.app.shared.feature.player
import com.fluxa.app.common.AppStrings import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.remote.Meta
import com.fluxa.app.data.remote.Stream
import com.fluxa.app.ui.catalog.DeviceType import com.fluxa.app.ui.catalog.DeviceType
import com.fluxa.app.ui.catalog.FluxaIcons import com.fluxa.app.ui.catalog.FluxaIcons
import com.fluxa.app.ui.catalog.LocalDeviceType import com.fluxa.app.ui.catalog.LocalDeviceType
import com.fluxa.app.ui.catalog.streamRawBody import com.fluxa.app.ui.catalog.StreamSourceUiModel
import com.fluxa.app.ui.catalog.streamSourceHeader
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
@ -50,7 +47,7 @@ import androidx.compose.ui.zIndex
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@Composable @Composable
fun SourceSidebar(streams: List<Stream>, currentUrl: String, deviceType: DeviceType, lang: String = "en", onSelect: (String) -> Unit, onClose: (() -> Unit)? = null) { fun SourceSidebar(streams: List<StreamSourceUiModel>, currentUrl: String, deviceType: DeviceType, lang: String = "en", onSelect: (String) -> Unit, onClose: (() -> Unit)? = null) {
PlayerSidebarShell( PlayerSidebarShell(
title = AppStrings.t(lang, "player.source_selection_title"), title = AppStrings.t(lang, "player.source_selection_title"),
deviceType = deviceType, deviceType = deviceType,
@ -58,14 +55,14 @@ fun SourceSidebar(streams: List<Stream>, currentUrl: String, deviceType: DeviceT
cardWidth = 420.dp cardWidth = 420.dp
) { ) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(2.dp)) { LazyColumn(verticalArrangement = Arrangement.spacedBy(2.dp)) {
items(streams, key = { it.playableUrl ?: (it.title.orEmpty() + it.name.orEmpty()) }) { stream -> items(streams, key = { it.playableUrl ?: it.header }) { stream ->
val playableUrl = stream.playableUrl val playableUrl = stream.playableUrl
TrackItem( TrackItem(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
title = stream.streamSourceHeader(), title = stream.header,
isSelected = stream.playableUrl == currentUrl, isSelected = stream.playableUrl == currentUrl,
onClick = { playableUrl?.let(onSelect) }, onClick = { playableUrl?.let(onSelect) },
subtitle = stream.streamRawBody(), subtitle = stream.body,
deviceType = deviceType, deviceType = deviceType,
leadingIcon = FluxaIcons.PlayArrow leadingIcon = FluxaIcons.PlayArrow
) )
@ -314,9 +311,3 @@ fun TrackItem(
} }
} }
} }
fun Meta.withCurrentEpisodeArtwork(artwork: String?): Meta {
val episodeArtwork = artwork?.takeIf { it.isNotBlank() } ?: return this
if (type != "series") return this
return copy(continueWatchingPoster = episodeArtwork, continueWatchingBackground = episodeArtwork)
}

View file

@ -1,8 +1,6 @@
package com.fluxa.app.shared.feature.player package com.fluxa.app.shared.feature.player
import com.fluxa.app.common.AppStrings import com.fluxa.app.common.AppStrings
import com.fluxa.app.data.local.UserProfile
import com.fluxa.app.player.MediaTrack
import com.fluxa.app.ui.catalog.DeviceType import com.fluxa.app.ui.catalog.DeviceType
import com.fluxa.app.ui.catalog.FluxaColors import com.fluxa.app.ui.catalog.FluxaColors
import com.fluxa.app.ui.catalog.FluxaIcons import com.fluxa.app.ui.catalog.FluxaIcons
@ -55,33 +53,6 @@ fun TrackSidebar(
} }
} }
@Composable
fun QuickSettingsSidebar(profile: UserProfile?, onUpdateProfile: (UserProfile) -> Unit, currentOffset: Long, onOffsetChange: (Long) -> Unit, deviceType: DeviceType, lang: String = "en", onClose: () -> Unit) {
PlayerSidebarShell(
title = AppStrings.t(lang, "player.quick_settings_title"),
deviceType = deviceType,
onClose = onClose
) {
Text(AppStrings.t(lang, "player.subtitle_sync_title"), color = Color.White.copy(alpha = 0.62f), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 12.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(22.dp))
.background(Color.White.copy(alpha = 0.04f))
.border(1.dp, Color.White.copy(alpha = 0.08f), RoundedCornerShape(22.dp))
.padding(18.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
SeekIconButton(FluxaIcons.Remove, deviceType) { onOffsetChange(currentOffset - 500) }
Text(text = "${if (currentOffset >= 0) "+" else ""}${currentOffset / 1000.0}s", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Black, modifier = Modifier.weight(1f), textAlign = TextAlign.Center)
SeekIconButton(FluxaIcons.Add, deviceType) { onOffsetChange(currentOffset + 500) }
}
}
Spacer(Modifier.height(8.dp))
}
}
@Composable @Composable
fun PlayerPremiumToggle(title: String, desc: String, isEnabled: Boolean, onToggle: () -> Unit) { fun PlayerPremiumToggle(title: String, desc: String, isEnabled: Boolean, onToggle: () -> Unit) {
Box(modifier = Modifier.fillMaxWidth().height(82.dp).clip(RoundedCornerShape(22.dp)).background(Color.White.copy(alpha = 0.05f)).border(1.dp, Color.White.copy(alpha = 0.08f), RoundedCornerShape(22.dp)).clickable { onToggle() }.padding(horizontal = 20.dp), contentAlignment = Alignment.CenterStart) { Box(modifier = Modifier.fillMaxWidth().height(82.dp).clip(RoundedCornerShape(22.dp)).background(Color.White.copy(alpha = 0.05f)).border(1.dp, Color.White.copy(alpha = 0.08f), RoundedCornerShape(22.dp)).clickable { onToggle() }.padding(horizontal = 20.dp), contentAlignment = Alignment.CenterStart) {