@@ -92,6 +92,7 @@ import androidx.media3.ui.PlayerView
9292import com.pira.ccloud.data.model.SubtitleSettings
9393import com.pira.ccloud.data.model.VideoPlayerSettings
9494import com.pira.ccloud.data.model.FontSettings
95+ import com.pira.ccloud.data.model.WatchedEpisode
9596import com.pira.ccloud.utils.StorageUtils
9697import com.pira.ccloud.ui.theme.FontManager
9798import kotlinx.coroutines.CoroutineScope
@@ -131,6 +132,9 @@ fun PlayerView.setSubtitleColors(settings: SubtitleSettings, typeface: Typeface?
131132class VideoPlayerActivity : ComponentActivity () {
132133 companion object {
133134 const val EXTRA_VIDEO_URL = " video_url"
135+ const val EXTRA_SERIES_ID = " series_id"
136+ const val EXTRA_SEASON_ID = " season_id"
137+ const val EXTRA_EPISODE_ID = " episode_id"
134138 const val REQUEST_WRITE_SETTINGS = 1001
135139
136140 fun start (context : Context , videoUrl : String ) {
@@ -139,12 +143,26 @@ class VideoPlayerActivity : ComponentActivity() {
139143 }
140144 context.startActivity(intent)
141145 }
146+
147+ fun startWithEpisodeInfo (context : Context , videoUrl : String , seriesId : Int , seasonId : Int , episodeId : Int ) {
148+ val intent = Intent (context, VideoPlayerActivity ::class .java).apply {
149+ putExtra(EXTRA_VIDEO_URL , videoUrl)
150+ putExtra(EXTRA_SERIES_ID , seriesId)
151+ putExtra(EXTRA_SEASON_ID , seasonId)
152+ putExtra(EXTRA_EPISODE_ID , episodeId)
153+ }
154+ context.startActivity(intent)
155+ }
142156 }
143157
144158 private var exoPlayer: ExoPlayer ? = null
145159 private var videoUrl: String? = null
160+ private var seriesId: Int? = null
161+ private var seasonId: Int? = null
162+ private var episodeId: Int? = null
146163 private var playerInitialized = false
147164 private var isActivityResumed = false
165+ private var hasMarkedAsWatched = false
148166
149167 override fun onCreate (savedInstanceState : Bundle ? ) {
150168 super .onCreate(savedInstanceState)
@@ -159,10 +177,19 @@ class VideoPlayerActivity : ComponentActivity() {
159177 window.addFlags(android.view.WindowManager .LayoutParams .FLAG_KEEP_SCREEN_ON )
160178
161179 videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL )
180+ seriesId = intent.getIntExtra(EXTRA_SERIES_ID , - 1 ).takeIf { it != - 1 }
181+ seasonId = intent.getIntExtra(EXTRA_SEASON_ID , - 1 ).takeIf { it != - 1 }
182+ episodeId = intent.getIntExtra(EXTRA_EPISODE_ID , - 1 ).takeIf { it != - 1 }
162183
163184 if (videoUrl != null ) {
164185 setContent {
165- VideoPlayerScreen (videoUrl!! , this ::finish) { player ->
186+ VideoPlayerScreen (
187+ videoUrl = videoUrl!! ,
188+ seriesId = seriesId,
189+ seasonId = seasonId,
190+ episodeId = episodeId,
191+ onBack = this ::finish
192+ ) { player ->
166193 exoPlayer = player
167194 playerInitialized = true
168195 }
@@ -306,6 +333,9 @@ class VideoPlayerActivity : ComponentActivity() {
306333@Composable
307334fun VideoPlayerScreen (
308335 videoUrl : String ,
336+ seriesId : Int? ,
337+ seasonId : Int? ,
338+ episodeId : Int? ,
309339 onBack : () -> Unit ,
310340 onPlayerReady : (ExoPlayer ) -> Unit
311341) {
@@ -323,6 +353,7 @@ fun VideoPlayerScreen(
323353 var playbackSpeed by remember { mutableStateOf(1.0f ) }
324354 var showSpeedDropdown by remember { mutableStateOf(false ) }
325355 var playerInitialized by remember { mutableStateOf(false ) }
356+ var hasMarkedAsWatched by remember { mutableStateOf(false ) }
326357
327358 // Track selection state
328359 var showTrackSelectionDialog by remember { mutableStateOf(false ) }
@@ -396,7 +427,7 @@ fun VideoPlayerScreen(
396427 if (isRetrying && currentPosition > 0 ) {
397428 seekTo(currentPosition)
398429 }
399- playWhenReady = true // Start playing by default
430+ playWhenReady = isPlaying // Start with current play state
400431 // Set initial playback speed
401432 setPlaybackSpeed(playbackSpeed)
402433 } catch (e: Exception ) {
@@ -450,10 +481,25 @@ fun VideoPlayerScreen(
450481 }
451482 }
452483
453- // Update player state
484+ // Update player state and mark episode as watched
454485 LaunchedEffect (isPlaying, exoPlayer) {
455486 try {
456487 exoPlayer?.playWhenReady = isPlaying
488+
489+ // Mark episode as watched when playback starts (only once)
490+ if (isPlaying && ! hasMarkedAsWatched && seriesId != null && seasonId != null && episodeId != null ) {
491+ try {
492+ val watchedEpisode = WatchedEpisode (
493+ seriesId = seriesId!! ,
494+ seasonId = seasonId!! ,
495+ episodeId = episodeId!!
496+ )
497+ StorageUtils .saveWatchedEpisode(context, watchedEpisode)
498+ hasMarkedAsWatched = true
499+ } catch (e: Exception ) {
500+ // Ignore storage errors
501+ }
502+ }
457503 } catch (e: Exception ) {
458504 // Ignore player state errors
459505 }
@@ -482,6 +528,15 @@ fun VideoPlayerScreen(
482528 try {
483529 if (playbackState == Player .STATE_READY ) {
484530 duration = exoPlayer?.duration ? : 0L
531+
532+ // After the player is ready (especially after a retry),
533+ // ensure the playWhenReady state is consistent with our UI state
534+ if (exoPlayer != null && ! isRetrying) {
535+ exoPlayer?.playWhenReady = isPlaying
536+ }
537+ } else if (playbackState == Player .STATE_ENDED ) {
538+ // Video ended, pause the player
539+ isPlaying = false
485540 }
486541 } catch (e: Exception ) {
487542 // Ignore duration errors
@@ -509,6 +564,7 @@ fun VideoPlayerScreen(
509564
510565 // Store current position before retrying
511566 val retryPosition = currentPosition
567+ val wasPlaying = isPlaying // Store whether it was playing before the error
512568
513569 // Attempt to retry after a delay
514570 CoroutineScope (Dispatchers .Main ).launch {
@@ -520,7 +576,12 @@ fun VideoPlayerScreen(
520576 player.prepare()
521577 // Seek to the stored position after preparing
522578 player.seekTo(retryPosition)
523- player.playWhenReady = true
579+
580+ // Resume playback if it was playing before the error
581+ player.playWhenReady = wasPlaying
582+
583+ // Update the UI state to match the player state
584+ isPlaying = wasPlaying
524585 isRetrying = false
525586 playerError = null
526587 }
@@ -963,13 +1024,17 @@ fun VideoPlayerScreen(
9631024 // Manual retry
9641025 try {
9651026 exoPlayer?.let { player ->
966- // Store current position before retrying
1027+ // Store current position and playback state before retrying
9671028 val retryPosition = currentPosition
1029+ val wasPlaying = isPlaying // Store whether it was playing before the retry
9681030 player.setMediaItem(MediaItem .fromUri(Uri .parse(videoUrl)))
9691031 player.prepare()
9701032 // Seek to the stored position after preparing
9711033 player.seekTo(retryPosition)
972- player.playWhenReady = true
1034+ // Resume playback if it was playing before the retry
1035+ player.playWhenReady = wasPlaying
1036+ // Update the UI state to match the player state
1037+ isPlaying = wasPlaying
9731038 isRetrying = false
9741039 playerError = null
9751040 }
0 commit comments