diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/AndroidCatalogHomeDataSource.kt b/app/src/main/java/com/fluxa/app/ui/catalog/AndroidCatalogHomeDataSource.kt index 51238ce..cdd9af9 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/AndroidCatalogHomeDataSource.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/AndroidCatalogHomeDataSource.kt @@ -81,7 +81,7 @@ class AndroidCatalogHomeDataSource( val items: List, val logoUrl: String?, val trailerUrl: String?, - val trailerSubtitleCues: List + val trailerSubtitleCues: List ) private fun billboardResolution(): Flow = combine( @@ -109,7 +109,7 @@ class AndroidCatalogHomeDataSource( billboardPool: List, billboardLogo: String?, billboardTrailerUrl: String?, - billboardTrailerSubtitleCues: List, + billboardTrailerSubtitleCues: List, filter: String ): BillboardResolution { val filteredPool = billboardPool.filter { it.matchesFilter(filter) } diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HeroTrailerVideoSurface.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HeroTrailerVideoSurface.kt index 41da3e0..0c4e832 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HeroTrailerVideoSurface.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HeroTrailerVideoSurface.kt @@ -14,7 +14,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.ui.AspectRatioFrameLayout 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.isActive diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardRuntime.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardRuntime.kt index 9ac0cd3..94ef5b2 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardRuntime.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardRuntime.kt @@ -1,9 +1,9 @@ package com.fluxa.app.ui.catalog -import com.fluxa.app.player.TrailerCue -import com.fluxa.app.player.TrailerResolveResult -import com.fluxa.app.player.TrailerResult -import com.fluxa.app.player.TrailerSubtitle +import com.fluxa.app.shared.feature.player.TrailerCue +import com.fluxa.app.shared.feature.player.TrailerResolveResult +import com.fluxa.app.shared.feature.player.TrailerResult +import com.fluxa.app.shared.feature.player.TrailerSubtitle import android.util.LruCache import com.fluxa.app.core.rust.FluxaCoreNative diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardStateHolder.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardStateHolder.kt index d9c8d0c..44e67e1 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardStateHolder.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeBillboardStateHolder.kt @@ -1,7 +1,7 @@ package com.fluxa.app.ui.catalog 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.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt index 8338d41..8de35a0 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt @@ -165,7 +165,7 @@ class HomeViewModel @Inject constructor( val billboardWatchlist: StateFlow = billboardState.watchlist val billboardNextEpisode: StateFlow = billboardState.nextEpisode val billboardTrailerUrl: StateFlow = billboardState.trailerUrl - val billboardTrailerSubtitleCues: StateFlow> = billboardState.trailerSubtitleCues + val billboardTrailerSubtitleCues: StateFlow> = billboardState.trailerSubtitleCues val billboardSeasonPosterUrl: StateFlow = billboardState.seasonPosterUrl private val _isDirectLoading = MutableStateFlow(false) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/NativeLibassSubtitleOverlay.kt b/app/src/main/java/com/fluxa/app/ui/catalog/NativeLibassSubtitleOverlay.kt index 53c7e7d..e8cd4e9 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/NativeLibassSubtitleOverlay.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/NativeLibassSubtitleOverlay.kt @@ -19,7 +19,7 @@ import com.fluxa.app.player.LibassDebugLog import com.fluxa.app.player.LibassRenderThread import com.fluxa.app.player.LibassVideoFrame 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.NativeAssTrack import com.fluxa.app.player.NativeLibassRenderer diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerLoadingEffects.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerLoadingEffects.kt index 5f9a2ff..6ea4ad5 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerLoadingEffects.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerLoadingEffects.kt @@ -26,7 +26,7 @@ import com.fluxa.app.player.PlayerEngine import com.fluxa.app.player.PlayerEngineRequest import com.fluxa.app.player.TorrentStreamManager 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.Dispatchers import kotlinx.coroutines.delay 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 d65f6f8..d816590 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 @@ -29,7 +29,7 @@ internal fun extractSeasonEpisode(videoId: String?): Pair? { @Composable internal fun PlayerUIContent( - content: PlayerContentUiModel, lang: String, duration: Long, position: Long, bufferedFraction: Float, chapters: List = emptyList(), isPlaying: Boolean, isBuffering: Boolean, hasStartedPlaying: Boolean, deviceType: DeviceType, + 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 = {}, isSwitchingAudioSource: Boolean = false, detailedStatus: String = "", episodeMetaLine: String? = null, streamDetailLine: String? = null, subtitlesEnabled: Boolean = false, technicalInfo: String? = null, diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackRuntimeEffects.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackRuntimeEffects.kt index f4d97f7..a3989ec 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackRuntimeEffects.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackRuntimeEffects.kt @@ -27,7 +27,7 @@ import com.fluxa.app.player.NativeAssTrack import com.fluxa.app.player.PlayerEngine import com.fluxa.app.player.PlayerEngineRequest 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.delay import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSideEffects.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSideEffects.kt index 02f62ca..dab4193 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSideEffects.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerPlaybackSideEffects.kt @@ -17,7 +17,7 @@ import com.fluxa.app.data.remote.Meta import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Video 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.delay import kotlinx.coroutines.launch 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 cc983e9..bbd2e86 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 @@ -36,16 +36,14 @@ import androidx.media3.ui.PlayerView import com.fluxa.app.R import com.fluxa.app.data.local.* 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.Video -import com.fluxa.app.player.MediaTrack +import com.fluxa.app.shared.feature.player.MediaTrack import com.fluxa.app.player.ExternalSubtitleTrack import com.fluxa.app.player.NativeAssTrack import com.fluxa.app.player.MpvAndroidSurfaceView import com.fluxa.app.player.MpvEmbeddedPlayer 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.shared.feature.player.ArtisticLoadingOverlay import com.fluxa.app.shared.feature.player.MarkSegmentSidebar @@ -118,7 +116,7 @@ internal fun BoxScope.PlayerPlaybackSurface( seekBackwardMs: Long, hasPreviousEpisode: Boolean, hasNextEpisode: Boolean, - nextEpisode: Video?, + nextEpisode: NextEpisodePreviewUiModel?, onPlayPrevious: () -> Unit, onPlayNext: () -> Unit, onCast: () -> Unit, @@ -128,11 +126,11 @@ internal fun BoxScope.PlayerPlaybackSurface( onClose: () -> Unit, onNextEpisodeCardShown: () -> Unit, timelinePosition: () -> Long, - skipSegments: List, - chapters: List = emptyList(), + skipSegments: List, + chapters: List = emptyList(), dismissedSkipSegments: Set, - onSkipSegment: (IntroTimestamps) -> Unit, - onDismissSegment: (IntroTimestamps) -> Unit, + onSkipSegment: (SkipSegmentUiModel) -> Unit, + onDismissSegment: (SkipSegmentUiModel) -> Unit, showSegmentSkipFeedback: Boolean, holdSpeedVisible: Boolean, showVolumeBar: Boolean, @@ -502,7 +500,7 @@ internal fun PlayerSettingsPanel( ) } else if (activeSettingsTab == 4) { SourceSidebar( - streams = currentStreams, + streams = currentStreams.map { it.toSourceUiModel() }, currentUrl = currentUrl.orEmpty(), deviceType = deviceType, lang = lang, diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreen.kt index dcfc16b..a6b9860 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreen.kt @@ -25,8 +25,7 @@ import com.fluxa.app.data.remote.Meta import com.fluxa.app.data.remote.Stream import com.fluxa.app.player.* import com.fluxa.app.player.MediaPlayerController -import com.fluxa.app.player.MediaTrack -import com.fluxa.app.shared.feature.player.dismissKey +import com.fluxa.app.shared.feature.player.MediaTrack import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenContent.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenContent.kt index d3545ab..67d1874 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenContent.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenContent.kt @@ -52,12 +52,11 @@ import com.fluxa.app.data.local.* import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.remote.Meta 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.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.dismissKey import com.fluxa.app.shared.feature.player.playerInputControls import com.fluxa.app.shared.feature.player.playerText @@ -288,7 +287,7 @@ internal fun PlayerScreenContent( seekBackwardMs = seekBackwardMs, hasPreviousEpisode = state.previousEpisodePending != null, hasNextEpisode = state.nextEpisodePending != null, - nextEpisode = state.nextEpisodePending, + nextEpisode = state.nextEpisodePending?.toNextEpisodePreviewUiModel(), onPlayPrevious = playPrevious, onPlayNext = playNext, onCast = smartCast, @@ -310,11 +309,11 @@ internal fun PlayerScreenContent( state.nextEpisodePending?.let { viewModel.onNextEpisodeCardShown(meta, it.id, activeProfile) } }, timelinePosition = { if (useMpvBackend) state.engine.timeline.position else state.timelinePosition }, - skipSegments = state.skipSegments, + skipSegments = state.skipSegments.map { it.toSkipSegmentUiModel() }, chapters = state.chapters, dismissedSkipSegments = state.dismissedSkipSegments, 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.introAutoSkipped = true state.segmentSkipFeedbackVersion += 1 diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenHelpers.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenHelpers.kt index 753359a..cdab263 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenHelpers.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenHelpers.kt @@ -79,7 +79,7 @@ import coil3.compose.AsyncImage import com.fluxa.app.R import com.fluxa.app.player.* 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.async import kotlinx.coroutines.awaitAll diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt index 93b500c..1f1300d 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt @@ -78,7 +78,7 @@ internal class PlayerScreenState( var currentEpisodeMetaLine by mutableStateOf(null) var currentEpisodeArtwork by mutableStateOf(null) var skipSegments by mutableStateOf>(emptyList()) - var chapters by mutableStateOf>(emptyList()) + var chapters by mutableStateOf>(emptyList()) var dismissedSkipSegments by mutableStateOf>(emptySet()) var introAutoSkipped by mutableStateOf(false) var preferredBingeGroupForNextEpisode by mutableStateOf(null) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSetupEffects.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSetupEffects.kt index 59aaae0..ba96c9a 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSetupEffects.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSetupEffects.kt @@ -22,7 +22,7 @@ import com.fluxa.app.player.PlayerEngine import com.fluxa.app.player.PlayerEngineRequest import com.fluxa.app.player.TorrentStreamManager 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.Dispatchers import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerTrackMemoryEffects.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerTrackMemoryEffects.kt index 0f4d75d..2a8928d 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerTrackMemoryEffects.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerTrackMemoryEffects.kt @@ -10,9 +10,9 @@ import com.fluxa.app.data.local.* import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.remote.Meta 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.shared.feature.player.withCurrentEpisodeArtwork +import com.fluxa.app.data.remote.withCurrentEpisodeArtwork import java.util.Locale @Composable diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/TrackSelectionState.kt b/app/src/main/java/com/fluxa/app/ui/catalog/TrackSelectionState.kt index c75d98b..bef4151 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/TrackSelectionState.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/TrackSelectionState.kt @@ -7,7 +7,7 @@ import com.fluxa.app.domain.discovery.* import com.fluxa.app.core.rust.FluxaCoreNative 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 java.util.Locale diff --git a/app/src/test/java/com/fluxa/app/player/MkvChapterFetcherTest.kt b/app/src/test/java/com/fluxa/app/player/MkvChapterFetcherTest.kt index 876c3a2..e73f230 100644 --- a/app/src/test/java/com/fluxa/app/player/MkvChapterFetcherTest.kt +++ b/app/src/test/java/com/fluxa/app/player/MkvChapterFetcherTest.kt @@ -1,5 +1,6 @@ package com.fluxa.app.player +import com.fluxa.app.shared.feature.player.Chapter import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/app/src/test/java/com/fluxa/app/ui/catalog/TrackSelectionStateTest.kt b/app/src/test/java/com/fluxa/app/ui/catalog/TrackSelectionStateTest.kt index facb67f..b7a348c 100644 --- a/app/src/test/java/com/fluxa/app/ui/catalog/TrackSelectionStateTest.kt +++ b/app/src/test/java/com/fluxa/app/ui/catalog/TrackSelectionStateTest.kt @@ -6,7 +6,7 @@ import com.fluxa.app.data.repository.* import com.fluxa.app.domain.discovery.* 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.assertSame import org.junit.Assert.assertTrue diff --git a/data/src/commonMain/kotlin/com/fluxa/app/data/remote/MetaModels.kt b/data/src/commonMain/kotlin/com/fluxa/app/data/remote/MetaModels.kt index f7507c2..8c95760 100644 --- a/data/src/commonMain/kotlin/com/fluxa/app/data/remote/MetaModels.kt +++ b/data/src/commonMain/kotlin/com/fluxa/app/data/remote/MetaModels.kt @@ -475,3 +475,9 @@ object MetaDetailSerializer : KSerializer { 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) +} diff --git a/data/src/commonMain/kotlin/com/fluxa/app/ui/catalog/PlayerOverlayUi.kt b/data/src/commonMain/kotlin/com/fluxa/app/ui/catalog/PlayerOverlayUi.kt new file mode 100644 index 0000000..6d7b13c --- /dev/null +++ b/data/src/commonMain/kotlin/com/fluxa/app/ui/catalog/PlayerOverlayUi.kt @@ -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 +) diff --git a/data/src/commonMain/kotlin/com/fluxa/app/ui/catalog/StreamSourceUi.kt b/data/src/commonMain/kotlin/com/fluxa/app/ui/catalog/StreamSourceUi.kt index 8824c4e..30b294b 100644 --- a/data/src/commonMain/kotlin/com/fluxa/app/ui/catalog/StreamSourceUi.kt +++ b/data/src/commonMain/kotlin/com/fluxa/app/ui/catalog/StreamSourceUi.kt @@ -2,6 +2,18 @@ package com.fluxa.app.ui.catalog 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 { return name ?.takeIf { it.isNotBlank() } diff --git a/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt b/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt index 5d60a89..7b54cc7 100644 --- a/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt +++ b/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt @@ -1,5 +1,7 @@ package com.fluxa.app.player +import com.fluxa.app.shared.feature.player.MediaTrack + import android.content.Context import android.util.Log import androidx.media3.common.* diff --git a/player/src/androidMain/kotlin/com/fluxa/app/player/MkvChapterFetcher.kt b/player/src/androidMain/kotlin/com/fluxa/app/player/MkvChapterFetcher.kt index a7de370..83e101c 100644 --- a/player/src/androidMain/kotlin/com/fluxa/app/player/MkvChapterFetcher.kt +++ b/player/src/androidMain/kotlin/com/fluxa/app/player/MkvChapterFetcher.kt @@ -1,5 +1,7 @@ package com.fluxa.app.player +import com.fluxa.app.shared.feature.player.Chapter + import com.fluxa.app.core.rust.FluxaStreamingNative import com.google.gson.Gson import kotlinx.coroutines.Dispatchers diff --git a/player/src/androidMain/kotlin/com/fluxa/app/player/MpvEmbeddedPlayer.kt b/player/src/androidMain/kotlin/com/fluxa/app/player/MpvEmbeddedPlayer.kt index 51af036..1cd8aaa 100644 --- a/player/src/androidMain/kotlin/com/fluxa/app/player/MpvEmbeddedPlayer.kt +++ b/player/src/androidMain/kotlin/com/fluxa/app/player/MpvEmbeddedPlayer.kt @@ -1,5 +1,8 @@ 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.graphics.Color import android.media.AudioFormat diff --git a/player/src/androidMain/kotlin/com/fluxa/app/player/PlayerEngine.kt b/player/src/androidMain/kotlin/com/fluxa/app/player/PlayerEngine.kt index 0f56398..ecda28a 100644 --- a/player/src/androidMain/kotlin/com/fluxa/app/player/PlayerEngine.kt +++ b/player/src/androidMain/kotlin/com/fluxa/app/player/PlayerEngine.kt @@ -2,6 +2,9 @@ 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.SeekParameters import androidx.media3.common.C diff --git a/player/src/androidMain/kotlin/com/fluxa/app/player/TorrentStreamManager.kt b/player/src/androidMain/kotlin/com/fluxa/app/player/TorrentStreamManager.kt index ec4cafb..624ebb8 100644 --- a/player/src/androidMain/kotlin/com/fluxa/app/player/TorrentStreamManager.kt +++ b/player/src/androidMain/kotlin/com/fluxa/app/player/TorrentStreamManager.kt @@ -1,5 +1,7 @@ package com.fluxa.app.player +import com.fluxa.app.shared.feature.player.TorrentStreamStatus + import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities diff --git a/player/src/commonMain/kotlin/com/fluxa/app/player/ChapterSkipSegments.kt b/player/src/commonMain/kotlin/com/fluxa/app/player/ChapterSkipSegments.kt index 9ec0961..ba7764d 100644 --- a/player/src/commonMain/kotlin/com/fluxa/app/player/ChapterSkipSegments.kt +++ b/player/src/commonMain/kotlin/com/fluxa/app/player/ChapterSkipSegments.kt @@ -1,5 +1,7 @@ package com.fluxa.app.player +import com.fluxa.app.shared.feature.player.Chapter + import com.fluxa.app.data.remote.IntroTimestamps private fun classifyChapterSkipType(title: String): String? { diff --git a/player/src/commonMain/kotlin/com/fluxa/app/player/Chapter.kt b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/Chapter.kt similarity index 60% rename from player/src/commonMain/kotlin/com/fluxa/app/player/Chapter.kt rename to player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/Chapter.kt index 9259178..461c15b 100644 --- a/player/src/commonMain/kotlin/com/fluxa/app/player/Chapter.kt +++ b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/Chapter.kt @@ -1,4 +1,4 @@ -package com.fluxa.app.player +package com.fluxa.app.shared.feature.player data class Chapter( val title: String, diff --git a/player/src/commonMain/kotlin/com/fluxa/app/player/MediaTrackModels.kt b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/MediaTrackModels.kt similarity index 97% rename from player/src/commonMain/kotlin/com/fluxa/app/player/MediaTrackModels.kt rename to player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/MediaTrackModels.kt index b0a5d3d..27ec80f 100644 --- a/player/src/commonMain/kotlin/com/fluxa/app/player/MediaTrackModels.kt +++ b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/MediaTrackModels.kt @@ -1,4 +1,4 @@ -package com.fluxa.app.player +package com.fluxa.app.shared.feature.player data class MediaTrack( val id: String, diff --git a/player/src/commonMain/kotlin/com/fluxa/app/player/TorrentStreamStatus.kt b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/TorrentStreamStatus.kt similarity index 81% rename from player/src/commonMain/kotlin/com/fluxa/app/player/TorrentStreamStatus.kt rename to player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/TorrentStreamStatus.kt index b578b47..2a440ce 100644 --- a/player/src/commonMain/kotlin/com/fluxa/app/player/TorrentStreamStatus.kt +++ b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/TorrentStreamStatus.kt @@ -1,4 +1,4 @@ -package com.fluxa.app.player +package com.fluxa.app.shared.feature.player data class TorrentStreamStatus( val bufferProgress: Int = 0, diff --git a/player/src/commonMain/kotlin/com/fluxa/app/player/TrailerModels.kt b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/TrailerModels.kt similarity index 94% rename from player/src/commonMain/kotlin/com/fluxa/app/player/TrailerModels.kt rename to player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/TrailerModels.kt index 75b2def..05ab27d 100644 --- a/player/src/commonMain/kotlin/com/fluxa/app/player/TrailerModels.kt +++ b/player/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/TrailerModels.kt @@ -1,4 +1,4 @@ -package com.fluxa.app.player +package com.fluxa.app.shared.feature.player data class TrailerSubtitle( val languageTag: String, diff --git a/player/src/commonTest/kotlin/com/fluxa/app/player/TrailerPolicyTest.kt b/player/src/commonTest/kotlin/com/fluxa/app/player/TrailerPolicyTest.kt index 031f997..bb49646 100644 --- a/player/src/commonTest/kotlin/com/fluxa/app/player/TrailerPolicyTest.kt +++ b/player/src/commonTest/kotlin/com/fluxa/app/player/TrailerPolicyTest.kt @@ -1,5 +1,7 @@ package com.fluxa.app.player +import com.fluxa.app.shared.feature.player.TrailerPolicy + import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt index 05b0e39..11e32d3 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaApp.kt @@ -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.PlayerRenderAction 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.FluxaColors import com.fluxa.app.ui.catalog.PosterActionSheet diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/HeroTrailerSurface.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/HeroTrailerSurface.kt index 8c6d5db..8bd7a99 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/HeroTrailerSurface.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/HeroTrailerSurface.kt @@ -3,6 +3,6 @@ package com.fluxa.app.shared import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf 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, (String) -> Unit, Modifier) -> Unit)?> { null } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt index d9add1e..58fbd92 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/CatalogContracts.kt @@ -1,6 +1,6 @@ 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 kotlinx.coroutines.flow.Flow diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerControlsChrome.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerControlsChrome.kt index 660f53a..1ed5966 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerControlsChrome.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerControlsChrome.kt @@ -2,7 +2,6 @@ package com.fluxa.app.shared.feature.player 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.FluxaColors import com.fluxa.app.ui.catalog.FluxaDimensions diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerOverlayCards.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerOverlayCards.kt index a7c0934..433a732 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerOverlayCards.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerOverlayCards.kt @@ -1,13 +1,12 @@ package com.fluxa.app.shared.feature.player 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.DeviceType import com.fluxa.app.ui.catalog.FluxaColors import com.fluxa.app.ui.catalog.FluxaDimensions import com.fluxa.app.ui.catalog.FluxaIcons +import com.fluxa.app.ui.catalog.NextEpisodePreviewUiModel import com.fluxa.app.ui.catalog.PlaybackSnapshot import androidx.compose.animation.core.FastOutSlowInEasing @@ -66,7 +65,7 @@ fun playerStatusText(lang: String?, value: String): String { fun SkipSegmentCard( deviceType: DeviceType, type: String, - nextEpisode: Video? = null, + nextEpisode: NextEpisodePreviewUiModel? = null, lang: String? = "en", autoAdvanceSeconds: Int? = null, onSkip: () -> Unit, @@ -126,7 +125,7 @@ fun SkipSegmentCard( @Composable private fun NextEpisodeSkipCard( deviceType: DeviceType, - episode: Video, + episode: NextEpisodePreviewUiModel, lang: String?, autoAdvanceSeconds: Int? = null, 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 number = episode.number ?: 0 val name = episode.name.orEmpty().trim() diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerRuntimeOverlays.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerRuntimeOverlays.kt index c4f5e27..4e133ab 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerRuntimeOverlays.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerRuntimeOverlays.kt @@ -1,10 +1,10 @@ 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.FluxaDimensions 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.core.FastOutSlowInEasing @@ -51,26 +51,26 @@ import androidx.compose.ui.zIndex @Composable fun PlayerSkipSegmentOverlay( currentPosition: Long, - skipSegments: List, + skipSegments: List, dismissedSkipSegments: Set, hasStartedPlaying: Boolean, showControls: Boolean, deviceType: DeviceType, - nextEpisode: Video?, + nextEpisode: NextEpisodePreviewUiModel?, nextEpisodeThresholdReached: Boolean, autoSkipSegments: Boolean, autoPlayCountdownSeconds: Int? = null, lang: String, - onSkipSegment: (IntroTimestamps) -> Unit, + onSkipSegment: (SkipSegmentUiModel) -> Unit, onPlayNextEpisode: () -> Unit, - onDismissSegment: (IntroTimestamps) -> Unit, + onDismissSegment: (SkipSegmentUiModel) -> Unit, onNextEpisodeCardShown: () -> Unit, modifier: Modifier = Modifier ) { val activeSkipSegment = remember(currentPosition, skipSegments, dismissedSkipSegments, hasStartedPlaying, autoSkipSegments) { if (!hasStartedPlaying || autoSkipSegments) null else skipSegments.find { segment -> - currentPosition in segment.startTime until segment.endTime && + currentPosition in segment.startMs until segment.endMs && segment.dismissKey() !in dismissedSkipSegments } } @@ -307,7 +307,3 @@ fun BoxScope.PlayerTransientOverlays( } } } - -fun IntroTimestamps.dismissKey(): String { - return "$type:$startTime:$endTime" -} 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 5eec27e..953a280 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 @@ -1,7 +1,6 @@ package com.fluxa.app.shared.feature.player 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.FluxaIcons 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 59e0f15..e63a331 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 @@ -1,13 +1,10 @@ package com.fluxa.app.shared.feature.player 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.FluxaIcons import com.fluxa.app.ui.catalog.LocalDeviceType -import com.fluxa.app.ui.catalog.streamRawBody -import com.fluxa.app.ui.catalog.streamSourceHeader +import com.fluxa.app.ui.catalog.StreamSourceUiModel import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.FastOutSlowInEasing @@ -50,7 +47,7 @@ import androidx.compose.ui.zIndex import kotlinx.coroutines.delay @Composable -fun SourceSidebar(streams: List, currentUrl: String, deviceType: DeviceType, lang: String = "en", onSelect: (String) -> Unit, onClose: (() -> Unit)? = null) { +fun SourceSidebar(streams: List, currentUrl: String, deviceType: DeviceType, lang: String = "en", onSelect: (String) -> Unit, onClose: (() -> Unit)? = null) { PlayerSidebarShell( title = AppStrings.t(lang, "player.source_selection_title"), deviceType = deviceType, @@ -58,14 +55,14 @@ fun SourceSidebar(streams: List, currentUrl: String, deviceType: DeviceT cardWidth = 420.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 TrackItem( modifier = Modifier.animateItem(), - title = stream.streamSourceHeader(), + title = stream.header, isSelected = stream.playableUrl == currentUrl, onClick = { playableUrl?.let(onSelect) }, - subtitle = stream.streamRawBody(), + subtitle = stream.body, deviceType = deviceType, 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) -} diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerTrackSidebars.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerTrackSidebars.kt index e77191c..6e90bae 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerTrackSidebars.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerTrackSidebars.kt @@ -1,8 +1,6 @@ package com.fluxa.app.shared.feature.player 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.FluxaColors 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 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) {