feat: multi-source player with episode selection and auto-hide controls
- VideoExtractor: extract PlaySource (source tabs) with episodes grouped per source - SiteConfig: add sourceSelector and sourceEpisodeGroupSelector CSS selectors - PlayerActivity: source tabs + episode list at bottom, auto-hide after 4s, tap to toggle - SettingsActivity: add source selector configuration fields - Fullscreen playback with ExoPlayer or WebView fallback
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package com.videoapp.tv
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -14,6 +17,7 @@ import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.videoapp.tv.data.ConfigRepository
|
||||
import com.videoapp.tv.engine.Episode
|
||||
import com.videoapp.tv.engine.PlaySource
|
||||
import com.videoapp.tv.engine.VideoExtractor
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -21,8 +25,9 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var playerView: PlayerView
|
||||
private lateinit var playerWebView: WebView
|
||||
private lateinit var episodePanel: View
|
||||
private lateinit var episodeList: android.widget.LinearLayout
|
||||
private lateinit var controlPanel: View
|
||||
private lateinit var sourceList: LinearLayout
|
||||
private lateinit var episodeList: LinearLayout
|
||||
private lateinit var loadingIndicator: View
|
||||
private lateinit var errorText: android.widget.TextView
|
||||
private lateinit var btnClose: ImageButton
|
||||
@@ -31,8 +36,13 @@ class PlayerActivity : AppCompatActivity() {
|
||||
private val videoExtractor = VideoExtractor()
|
||||
private val configRepo by lazy { ConfigRepository(this) }
|
||||
|
||||
private var episodes: List<Episode> = emptyList()
|
||||
private var currentEpisodeIndex = 0
|
||||
private var sources: List<PlaySource> = emptyList()
|
||||
private var currentSourceIndex = 0
|
||||
private var currentEpisode: Episode? = null
|
||||
|
||||
private val hideHandler = Handler(Looper.getMainLooper())
|
||||
private val hideRunnable = Runnable { hideControls() }
|
||||
private var controlsVisible = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -40,19 +50,19 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
playerView = findViewById(R.id.player_view)
|
||||
playerWebView = findViewById(R.id.player_webview)
|
||||
episodePanel = findViewById(R.id.episode_panel)
|
||||
controlPanel = findViewById(R.id.control_panel)
|
||||
sourceList = findViewById(R.id.source_list)
|
||||
episodeList = findViewById(R.id.episode_list)
|
||||
loadingIndicator = findViewById(R.id.loading_indicator)
|
||||
errorText = findViewById(R.id.error_text)
|
||||
btnClose = findViewById(R.id.btn_close)
|
||||
|
||||
val detailUrl = intent.getStringExtra("detail_url") ?: ""
|
||||
val title = intent.getStringExtra("title") ?: ""
|
||||
|
||||
btnClose.setOnClickListener { finish() }
|
||||
|
||||
initExoPlayer()
|
||||
loadVideos(detailUrl, title)
|
||||
loadSources(detailUrl)
|
||||
setupTouchListeners()
|
||||
}
|
||||
|
||||
@@ -62,36 +72,33 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVideos(detailUrl: String, @Suppress("UNUSED_PARAMETER") title: String) {
|
||||
private fun loadSources(detailUrl: String) {
|
||||
showLoading(true)
|
||||
val config = configRepo.getConfig()
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val videoInfo = videoExtractor.extractVideos(detailUrl, config)
|
||||
episodes = videoInfo.episodes
|
||||
sources = videoExtractor.extractVideos(detailUrl, config)
|
||||
|
||||
if (videoInfo.episodes.isNotEmpty()) {
|
||||
buildEpisodeUI(videoInfo.episodes)
|
||||
playEpisode(videoInfo.episodes.first())
|
||||
if (sources.isNotEmpty()) {
|
||||
buildSourceUI()
|
||||
selectSource(0)
|
||||
resetAutoHide()
|
||||
} else {
|
||||
episodePanel.visibility = View.GONE
|
||||
tryPlayDirectly(detailUrl, config)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showError("加载视频失败: ${e.message}")
|
||||
showError("加载失败: ${e.message}")
|
||||
showLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEpisodeUI(eps: List<Episode>) {
|
||||
episodeList.removeAllViews()
|
||||
episodePanel.visibility = View.VISIBLE
|
||||
|
||||
eps.forEachIndexed { index, ep ->
|
||||
private fun buildSourceUI() {
|
||||
sourceList.removeAllViews()
|
||||
sources.forEachIndexed { index, source ->
|
||||
val btn = Button(this).apply {
|
||||
text = ep.title
|
||||
text = source.name
|
||||
setBackgroundResource(R.drawable.episode_selector)
|
||||
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
|
||||
textSize = 13f
|
||||
@@ -100,9 +107,49 @@ class PlayerActivity : AppCompatActivity() {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
setOnClickListener {
|
||||
currentEpisodeIndex = index
|
||||
selectSource(index)
|
||||
resetAutoHide()
|
||||
}
|
||||
}
|
||||
sourceList.addView(btn)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectSource(index: Int) {
|
||||
if (index !in sources.indices) return
|
||||
currentSourceIndex = index
|
||||
|
||||
// Highlight selected source
|
||||
for (i in 0 until sourceList.childCount) {
|
||||
sourceList.getChildAt(i).isSelected = (i == index)
|
||||
}
|
||||
|
||||
// Build episode list for this source
|
||||
val source = sources[index]
|
||||
buildEpisodeUI(source.episodes)
|
||||
|
||||
// Auto-play first episode
|
||||
if (source.episodes.isNotEmpty()) {
|
||||
playEpisode(source.episodes.first())
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEpisodeUI(eps: List<Episode>) {
|
||||
episodeList.removeAllViews()
|
||||
eps.forEach { ep ->
|
||||
val btn = Button(this).apply {
|
||||
text = ep.title
|
||||
setBackgroundResource(R.drawable.episode_selector)
|
||||
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
|
||||
textSize = 12f
|
||||
minWidth = 0
|
||||
setPadding(14, 6, 14, 6)
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
setOnClickListener {
|
||||
highlightEpisode(it)
|
||||
playEpisode(ep)
|
||||
resetAutoHide()
|
||||
}
|
||||
}
|
||||
episodeList.addView(btn)
|
||||
@@ -110,6 +157,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun playEpisode(ep: Episode) {
|
||||
currentEpisode = ep
|
||||
showLoading(true)
|
||||
val config = configRepo.getConfig()
|
||||
|
||||
@@ -130,10 +178,13 @@ class PlayerActivity : AppCompatActivity() {
|
||||
lifecycleScope.launch {
|
||||
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
|
||||
if (directUrl != null) {
|
||||
controlPanel.visibility = View.GONE
|
||||
playWithExoPlayer(directUrl)
|
||||
} else if (iframeUrl != null) {
|
||||
controlPanel.visibility = View.GONE
|
||||
playWithWebView(iframeUrl)
|
||||
} else {
|
||||
controlPanel.visibility = View.GONE
|
||||
playWithWebView(detailUrl)
|
||||
}
|
||||
}
|
||||
@@ -186,15 +237,38 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setupTouchListeners() {
|
||||
var panelVisible = true
|
||||
playerView.setOnClickListener {
|
||||
panelVisible = !panelVisible
|
||||
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
||||
val listener = View.OnClickListener { toggleControls() }
|
||||
playerView.setOnClickListener(listener)
|
||||
playerWebView.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
private fun toggleControls() {
|
||||
if (controlsVisible) {
|
||||
hideControls()
|
||||
} else {
|
||||
showControls()
|
||||
resetAutoHide()
|
||||
}
|
||||
playerWebView.setOnClickListener {
|
||||
panelVisible = !panelVisible
|
||||
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun showControls() {
|
||||
controlsVisible = true
|
||||
btnClose.visibility = View.VISIBLE
|
||||
controlPanel.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun hideControls() {
|
||||
controlsVisible = false
|
||||
btnClose.visibility = View.GONE
|
||||
controlPanel.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun resetAutoHide() {
|
||||
hideHandler.removeCallbacks(hideRunnable)
|
||||
if (!controlsVisible) {
|
||||
showControls()
|
||||
}
|
||||
hideHandler.postDelayed(hideRunnable, 4000)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -204,6 +278,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
hideHandler.removeCallbacks(hideRunnable)
|
||||
exoPlayer?.release()
|
||||
exoPlayer = null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user