feat: 暂停显示选集、亮度音量手势调节、播放进度续播
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
package com.videoapp.tv
|
package com.videoapp.tv
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
@@ -10,10 +13,12 @@ import android.widget.Button
|
|||||||
import android.widget.HorizontalScrollView
|
import android.widget.HorizontalScrollView
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import com.videoapp.tv.data.AppDatabase
|
import com.videoapp.tv.data.AppDatabase
|
||||||
@@ -33,29 +38,49 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
private lateinit var episodeList: LinearLayout
|
private lateinit var episodeList: LinearLayout
|
||||||
private lateinit var episodeScroll: HorizontalScrollView
|
private lateinit var episodeScroll: HorizontalScrollView
|
||||||
private lateinit var loadingIndicator: View
|
private lateinit var loadingIndicator: View
|
||||||
private lateinit var errorText: android.widget.TextView
|
private lateinit var errorText: TextView
|
||||||
private lateinit var btnClose: ImageButton
|
private lateinit var btnClose: ImageButton
|
||||||
|
private lateinit var brightnessVolumeIndicator: TextView
|
||||||
|
|
||||||
private var exoPlayer: ExoPlayer? = null
|
private var exoPlayer: ExoPlayer? = null
|
||||||
private val videoExtractor = VideoExtractor()
|
private val videoExtractor = VideoExtractor()
|
||||||
private val configRepo by lazy { ConfigRepository(this) }
|
private val configRepo by lazy { ConfigRepository(this) }
|
||||||
private val playHistoryDao by lazy { AppDatabase.getInstance(this).playHistoryDao() }
|
private val playHistoryDao by lazy { AppDatabase.getInstance(this).playHistoryDao() }
|
||||||
|
private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager }
|
||||||
|
|
||||||
private var sources: List<PlaySource> = emptyList()
|
private var sources: List<PlaySource> = emptyList()
|
||||||
private var currentSourceIndex = 0
|
private var currentSourceIndex = 0
|
||||||
private var currentEpisode: Episode? = null
|
private var currentEpisode: Episode? = null
|
||||||
private var historyEpisode: String? = null
|
private var historyEpisode: String? = null
|
||||||
|
private var resumePosition: Long? = null
|
||||||
|
private var currentPlayHistoryId: Long? = null
|
||||||
|
|
||||||
private val hideHandler = Handler(Looper.getMainLooper())
|
private val hideHandler = Handler(Looper.getMainLooper())
|
||||||
private val hideRunnable = Runnable { hideControls() }
|
private val hideRunnable = Runnable { hideControls() }
|
||||||
private var controlsVisible = true
|
private var controlsVisible = true
|
||||||
|
|
||||||
// 用于保存播放历史的信息
|
private val positionSaveHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val positionSaveRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
saveCurrentPosition()
|
||||||
|
positionSaveHandler.postDelayed(this, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var videoTitle: String = ""
|
private var videoTitle: String = ""
|
||||||
private var videoCategory: String? = null
|
private var videoCategory: String? = null
|
||||||
private var coverUrl: String? = null
|
private var coverUrl: String? = null
|
||||||
private var detailUrl: String = ""
|
private var detailUrl: String = ""
|
||||||
|
|
||||||
|
private var touchStartY = 0f
|
||||||
|
private var touchStartX = 0f
|
||||||
|
private var isAdjusting = false
|
||||||
|
private var isBrightnessMode = false
|
||||||
|
private var startBrightness = 0f
|
||||||
|
private var startVolume = 0
|
||||||
|
private var maxVolume = 0
|
||||||
|
private val indicatorHideHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_player)
|
setContentView(R.layout.activity_player)
|
||||||
@@ -69,12 +94,16 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
loadingIndicator = findViewById(R.id.loading_indicator)
|
loadingIndicator = findViewById(R.id.loading_indicator)
|
||||||
errorText = findViewById(R.id.error_text)
|
errorText = findViewById(R.id.error_text)
|
||||||
btnClose = findViewById(R.id.btn_close)
|
btnClose = findViewById(R.id.btn_close)
|
||||||
|
brightnessVolumeIndicator = findViewById(R.id.brightness_volume_indicator)
|
||||||
|
|
||||||
detailUrl = intent.getStringExtra("detail_url") ?: ""
|
detailUrl = intent.getStringExtra("detail_url") ?: ""
|
||||||
videoTitle = intent.getStringExtra("title") ?: ""
|
videoTitle = intent.getStringExtra("title") ?: ""
|
||||||
videoCategory = intent.getStringExtra("category")
|
videoCategory = intent.getStringExtra("category")
|
||||||
coverUrl = intent.getStringExtra("cover_url")
|
coverUrl = intent.getStringExtra("cover_url")
|
||||||
historyEpisode = intent.getStringExtra("history_episode")
|
historyEpisode = intent.getStringExtra("history_episode")
|
||||||
|
resumePosition = if (intent.hasExtra("resume_position")) intent.getLongExtra("resume_position", 0) else null
|
||||||
|
|
||||||
|
maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
|
||||||
btnClose.setOnClickListener { finish() }
|
btnClose.setOnClickListener { finish() }
|
||||||
|
|
||||||
@@ -86,6 +115,16 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
private fun initExoPlayer() {
|
private fun initExoPlayer() {
|
||||||
exoPlayer = ExoPlayer.Builder(this).build().also { player ->
|
exoPlayer = ExoPlayer.Builder(this).build().also { player ->
|
||||||
playerView.player = player
|
playerView.player = player
|
||||||
|
player.addListener(object : Player.Listener {
|
||||||
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||||
|
if (playWhenReady) {
|
||||||
|
hideHandler.postDelayed(hideRunnable, 4000)
|
||||||
|
} else {
|
||||||
|
hideHandler.removeCallbacks(hideRunnable)
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +232,6 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
showLoading(true)
|
showLoading(true)
|
||||||
val config = configRepo.getConfig()
|
val config = configRepo.getConfig()
|
||||||
|
|
||||||
// 保存播放历史
|
|
||||||
savePlayHistory(ep.title)
|
savePlayHistory(ep.title)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -212,13 +250,11 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
private fun savePlayHistory(episodeName: String?) {
|
private fun savePlayHistory(episodeName: String?) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
// 检查是否已存在相同的播放记录
|
|
||||||
val existing = playHistoryDao.findByDetailUrl(detailUrl)
|
val existing = playHistoryDao.findByDetailUrl(detailUrl)
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
// 更新现有记录的播放时间和剧集
|
|
||||||
playHistoryDao.updatePlayTime(existing.id, System.currentTimeMillis(), episodeName)
|
playHistoryDao.updatePlayTime(existing.id, System.currentTimeMillis(), episodeName)
|
||||||
|
currentPlayHistoryId = existing.id
|
||||||
} else {
|
} else {
|
||||||
// 插入新记录
|
|
||||||
val playHistory = PlayHistory(
|
val playHistory = PlayHistory(
|
||||||
title = videoTitle,
|
title = videoTitle,
|
||||||
episodeName = episodeName,
|
episodeName = episodeName,
|
||||||
@@ -227,19 +263,43 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
category = videoCategory,
|
category = videoCategory,
|
||||||
playTime = System.currentTimeMillis()
|
playTime = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
playHistoryDao.insert(playHistory)
|
val id = playHistoryDao.insert(playHistory)
|
||||||
|
currentPlayHistoryId = id
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 保存播放历史失败不影响播放功能
|
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveCurrentPosition() {
|
||||||
|
val player = exoPlayer ?: return
|
||||||
|
val historyId = currentPlayHistoryId ?: return
|
||||||
|
if (playerView.visibility != View.VISIBLE) return
|
||||||
|
val pos = player.currentPosition
|
||||||
|
if (pos > 0) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
playHistoryDao.updatePosition(historyId, pos)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPositionSaving() {
|
||||||
|
positionSaveHandler.removeCallbacks(positionSaveRunnable)
|
||||||
|
positionSaveHandler.postDelayed(positionSaveRunnable, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopPositionSaving() {
|
||||||
|
positionSaveHandler.removeCallbacks(positionSaveRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
private fun tryPlayDirectly(detailUrl: String, config: com.videoapp.tv.data.SiteConfig) {
|
private fun tryPlayDirectly(detailUrl: String, config: com.videoapp.tv.data.SiteConfig) {
|
||||||
// 保存播放历史(直接播放时使用标题作为剧集名)
|
|
||||||
savePlayHistory(videoTitle)
|
savePlayHistory(videoTitle)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
|
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
|
||||||
if (directUrl != null) {
|
if (directUrl != null) {
|
||||||
@@ -265,10 +325,17 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
setMediaItem(mediaItem)
|
setMediaItem(mediaItem)
|
||||||
prepare()
|
prepare()
|
||||||
playWhenReady = true
|
playWhenReady = true
|
||||||
|
|
||||||
|
if (resumePosition != null && resumePosition!! > 0) {
|
||||||
|
seekTo(resumePosition!!)
|
||||||
|
resumePosition = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
startPositionSaving()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playWithWebView(url: String) {
|
private fun playWithWebView(url: String) {
|
||||||
|
stopPositionSaving()
|
||||||
playerView.visibility = View.GONE
|
playerView.visibility = View.GONE
|
||||||
playerWebView.visibility = View.VISIBLE
|
playerWebView.visibility = View.VISIBLE
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
@@ -302,9 +369,60 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTouchListeners() {
|
private fun setupTouchListeners() {
|
||||||
val listener = View.OnClickListener { toggleControls() }
|
val touchListener = View.OnTouchListener { view, event ->
|
||||||
playerView.setOnClickListener(listener)
|
when (event.action) {
|
||||||
playerWebView.setOnClickListener(listener)
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
touchStartX = event.x
|
||||||
|
touchStartY = event.y
|
||||||
|
isAdjusting = false
|
||||||
|
val halfWidth = view.width / 2
|
||||||
|
isBrightnessMode = touchStartX < halfWidth
|
||||||
|
startBrightness = window.attributes.screenBrightness
|
||||||
|
if (startBrightness < 0) startBrightness = 0.5f
|
||||||
|
startVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val deltaY = touchStartY - event.y
|
||||||
|
if (kotlin.math.abs(event.y - touchStartY) > 10f) {
|
||||||
|
isAdjusting = true
|
||||||
|
}
|
||||||
|
if (isAdjusting) {
|
||||||
|
if (isBrightnessMode) {
|
||||||
|
val newBrightness = (startBrightness + deltaY / view.height).coerceIn(0.01f, 1.0f)
|
||||||
|
window.attributes.screenBrightness = newBrightness
|
||||||
|
window.attributes = window.attributes
|
||||||
|
showIndicator("亮度", (newBrightness * 100).toInt())
|
||||||
|
} else {
|
||||||
|
val range = maxVolume.toFloat()
|
||||||
|
val newVolume = (startVolume + (deltaY / view.height) * range).toInt().coerceIn(0, maxVolume)
|
||||||
|
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0)
|
||||||
|
showIndicator("音量", (newVolume * 100 / maxVolume))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
val movedFar = kotlin.math.abs(event.x - touchStartX) > 20f || kotlin.math.abs(event.y - touchStartY) > 20f
|
||||||
|
if (!isAdjusting && !movedFar) {
|
||||||
|
toggleControls()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playerView.setOnTouchListener(touchListener)
|
||||||
|
playerWebView.setOnTouchListener(touchListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showIndicator(label: String, percent: Int) {
|
||||||
|
brightnessVolumeIndicator.text = "$label: $percent%"
|
||||||
|
brightnessVolumeIndicator.visibility = View.VISIBLE
|
||||||
|
indicatorHideHandler.removeCallbacksAndMessages(null)
|
||||||
|
indicatorHideHandler.postDelayed({
|
||||||
|
brightnessVolumeIndicator.visibility = View.GONE
|
||||||
|
}, 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleControls() {
|
private fun toggleControls() {
|
||||||
@@ -338,11 +456,15 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
saveCurrentPosition()
|
||||||
|
stopPositionSaving()
|
||||||
exoPlayer?.playWhenReady = false
|
exoPlayer?.playWhenReady = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
saveCurrentPosition()
|
||||||
|
stopPositionSaving()
|
||||||
hideHandler.removeCallbacks(hideRunnable)
|
hideHandler.removeCallbacks(hideRunnable)
|
||||||
exoPlayer?.release()
|
exoPlayer?.release()
|
||||||
exoPlayer = null
|
exoPlayer = null
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ data class PlayHistory(
|
|||||||
val detailUrl: String,
|
val detailUrl: String,
|
||||||
val coverUrl: String? = null,
|
val coverUrl: String? = null,
|
||||||
val category: String? = null,
|
val category: String? = null,
|
||||||
val playTime: Long = System.currentTimeMillis()
|
val playTime: Long = System.currentTimeMillis(),
|
||||||
|
val playbackPosition: Long? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface PlayHistoryDao {
|
|||||||
fun getRecentPlayHistory(): Flow<List<PlayHistory>>
|
fun getRecentPlayHistory(): Flow<List<PlayHistory>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(playHistory: PlayHistory)
|
suspend fun insert(playHistory: PlayHistory): Long
|
||||||
|
|
||||||
@Query("DELETE FROM play_history WHERE detailUrl = :detailUrl")
|
@Query("DELETE FROM play_history WHERE detailUrl = :detailUrl")
|
||||||
suspend fun deleteByDetailUrl(detailUrl: String)
|
suspend fun deleteByDetailUrl(detailUrl: String)
|
||||||
@@ -26,4 +26,7 @@ interface PlayHistoryDao {
|
|||||||
|
|
||||||
@Query("UPDATE play_history SET playTime = :playTime, episodeName = :episodeName WHERE id = :id")
|
@Query("UPDATE play_history SET playTime = :playTime, episodeName = :episodeName WHERE id = :id")
|
||||||
suspend fun updatePlayTime(id: Long, playTime: Long, episodeName: String?)
|
suspend fun updatePlayTime(id: Long, playTime: Long, episodeName: String?)
|
||||||
|
|
||||||
|
@Query("UPDATE play_history SET playbackPosition = :position WHERE id = :id")
|
||||||
|
suspend fun updatePosition(id: Long, position: Long)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,6 +226,9 @@ class SearchFragment : Fragment() {
|
|||||||
putExtra("category", playHistory.category)
|
putExtra("category", playHistory.category)
|
||||||
putExtra("cover_url", playHistory.coverUrl)
|
putExtra("cover_url", playHistory.coverUrl)
|
||||||
putExtra("history_episode", playHistory.episodeName)
|
putExtra("history_episode", playHistory.episodeName)
|
||||||
|
if (playHistory.playbackPosition != null && playHistory.playbackPosition > 0) {
|
||||||
|
putExtra("resume_position", playHistory.playbackPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,4 +89,15 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/brightness_volume_indicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="#CC000000"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
85
docs/superpowers/plans/2026-05-27-player-enhancements.md
Normal file
85
docs/superpowers/plans/2026-05-27-player-enhancements.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Player Enhancements Implementation Plan
|
||||||
|
|
||||||
|
> **Goal:** Add pause-to-show-controls, brightness/volume gestures, and playback position resume to PlayerActivity.
|
||||||
|
|
||||||
|
**Architecture:** Touch gesture detection on playerView, ExoPlayer listener for pause state, PlayHistory entity extension for position, AudioManager for volume, Window attributes for brightness.
|
||||||
|
|
||||||
|
**Tech Stack:** Kotlin, Android, ExoPlayer Media3, Room, AudioManager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add playbackPosition to PlayHistory entity and DAO
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/java/com/videoapp/tv/data/PlayHistory.kt`
|
||||||
|
- Modify: `app/src/main/java/com/videoapp/tv/data/PlayHistoryDao.kt`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add playbackPosition field to PlayHistory entity**
|
||||||
|
|
||||||
|
Add `val playbackPosition: Long? = null` to the data class. Since the DB is `fallbackToDestructiveMigration()`, no migration needed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add updatePosition method to PlayHistoryDao**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Query("UPDATE play_history SET playbackPosition = :position WHERE id = :id")
|
||||||
|
suspend fun updatePosition(id: Long, position: Long)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Save and resume playback position in PlayerActivity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/java/com/videoapp/tv/PlayerActivity.kt`
|
||||||
|
- Modify: `app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add fields and read resume position**
|
||||||
|
|
||||||
|
Add `resumePosition: Long?` field, read from intent `"resume_position"`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Modify playWithExoPlayer to seek to saved position**
|
||||||
|
|
||||||
|
After `prepare()`, add a listener to seek to `resumePosition` when ready, then clear `resumePosition`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add periodic position saving**
|
||||||
|
|
||||||
|
Add a `positionSaveHandler` and `positionSaveRunnable` that saves current position every 5 seconds.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Save position on pause/destroy**
|
||||||
|
|
||||||
|
In `onPause` and `onDestroy`, save current playback position before releasing player.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Pass playbackPosition from SearchFragment**
|
||||||
|
|
||||||
|
In `openPlayerFromHistory`, pass `playHistory.playbackPosition` as `"resume_position"`.
|
||||||
|
|
||||||
|
### Task 3: Pause shows controls
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/java/com/videoapp/tv/PlayerActivity.kt`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add ExoPlayer Listener for playWhenReady changes**
|
||||||
|
|
||||||
|
In `initExoPlayer`, add a `Player.Listener` that calls `showControls()` on pause and restarts auto-hide on play.
|
||||||
|
|
||||||
|
### Task 4: Brightness/Volume touch gestures
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/src/main/java/com/videoapp/tv/PlayerActivity.kt`
|
||||||
|
- Modify: `app/src/main/res/layout/activity_player.xml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace click listener with touch listener on playerView**
|
||||||
|
|
||||||
|
Replace `OnClickListener` with `OnTouchListener`. Track `startY`, `isAdjusting`, `isBrightnessMode`, `startBrightness`, `startVolume`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Handle ACTION_DOWN / MOVE / UP**
|
||||||
|
|
||||||
|
- DOWN: record startY, check x < width/2 for brightness mode
|
||||||
|
- MOVE: calculate delta, adjust brightness or volume, show indicator
|
||||||
|
- UP: if delta < threshold (20px), treat as click → toggleControls
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add floating indicator overlay TextView in layout**
|
||||||
|
|
||||||
|
Add a `brightness_volume_indicator` TextView to activity_player.xml, centered, initially GONE.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement showIndicator method**
|
||||||
|
|
||||||
|
Show the indicator with current brightness/volume percentage for 1 second after adjustment stops.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Player Enhancements Design
|
||||||
|
|
||||||
|
## 1. Pause Shows Controls
|
||||||
|
|
||||||
|
- Add `Player.Listener` to ExoPlayer, monitor `onPlayWhenReadyChanged`
|
||||||
|
- When `playWhenReady` becomes false: show control panel, cancel auto-hide timer
|
||||||
|
- When `playWhenReady` becomes true: restart auto-hide timer
|
||||||
|
- WebView playback: not supported (can't detect pause)
|
||||||
|
|
||||||
|
## 2. Brightness/Volume Gestures
|
||||||
|
|
||||||
|
- Replace click listener with `OnTouchListener` on playerView/playerWebView
|
||||||
|
- `ACTION_DOWN`: record startY, determine left/right half of screen
|
||||||
|
- Left half: brightness mode
|
||||||
|
- Right half: volume mode
|
||||||
|
- `ACTION_MOVE`: calculate deltaY, adjust brightness/volume proportionally
|
||||||
|
- `ACTION_UP`: if delta < threshold, treat as click (toggle controls); reset state
|
||||||
|
- Brightness: `window.attributes.screenBrightness` (activity-scoped, no permission)
|
||||||
|
- Volume: `AudioManager.setStreamVolume(STREAM_MUSIC, ...)`
|
||||||
|
- Show floating indicator (TextView overlay) during adjustment
|
||||||
|
|
||||||
|
## 3. Playback Position Resume
|
||||||
|
|
||||||
|
- Add `playbackPosition: Long?` to `PlayHistory` entity
|
||||||
|
- Add `updatePosition(id, position)` to `PlayHistoryDao`
|
||||||
|
- Save position every 5 seconds via handler, and on pause/destroy
|
||||||
|
- On player entry: read position from history, seek to it after prepare
|
||||||
|
- Pass `resume_position` via Intent from SearchFragment
|
||||||
|
- ExoPlayer only (WebView excluded)
|
||||||
Reference in New Issue
Block a user