package com.videoapp.tv import android.content.Context import android.media.AudioManager import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.MotionEvent import android.view.View import android.webkit.WebView import android.webkit.WebViewClient import android.widget.Button import android.widget.HorizontalScrollView import android.widget.ImageButton import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView import com.videoapp.tv.data.AppDatabase import com.videoapp.tv.data.ConfigRepository import com.videoapp.tv.data.PlayHistory import com.videoapp.tv.engine.Episode import com.videoapp.tv.engine.PlaySource import com.videoapp.tv.engine.VideoExtractor import kotlinx.coroutines.launch class PlayerActivity : AppCompatActivity() { private lateinit var playerView: PlayerView private lateinit var playerWebView: WebView private lateinit var controlPanel: View private lateinit var sourceList: LinearLayout private lateinit var episodeList: LinearLayout private lateinit var episodeScroll: HorizontalScrollView private lateinit var loadingIndicator: View private lateinit var errorText: TextView private lateinit var btnClose: ImageButton private lateinit var brightnessVolumeIndicator: TextView private var exoPlayer: ExoPlayer? = null private val videoExtractor = VideoExtractor() private val configRepo by lazy { ConfigRepository(this) } private val playHistoryDao by lazy { AppDatabase.getInstance(this).playHistoryDao() } private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager } private var sources: List = emptyList() private var currentSourceIndex = 0 private var currentEpisode: Episode? = null private var historyEpisode: String? = null private var resumePosition: Long? = null private var currentPlayHistoryId: Long? = null private val hideHandler = Handler(Looper.getMainLooper()) private val hideRunnable = Runnable { hideControls() } 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 videoCategory: String? = null private var coverUrl: String? = null 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?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_player) playerView = findViewById(R.id.player_view) playerWebView = findViewById(R.id.player_webview) controlPanel = findViewById(R.id.control_panel) sourceList = findViewById(R.id.source_list) episodeList = findViewById(R.id.episode_list) episodeScroll = findViewById(R.id.episode_scroll) loadingIndicator = findViewById(R.id.loading_indicator) errorText = findViewById(R.id.error_text) btnClose = findViewById(R.id.btn_close) brightnessVolumeIndicator = findViewById(R.id.brightness_volume_indicator) detailUrl = intent.getStringExtra("detail_url") ?: "" videoTitle = intent.getStringExtra("title") ?: "" videoCategory = intent.getStringExtra("category") coverUrl = intent.getStringExtra("cover_url") 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() } initExoPlayer() loadSources(detailUrl) setupTouchListeners() } private fun initExoPlayer() { exoPlayer = ExoPlayer.Builder(this).build().also { 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() } } }) } } private fun loadSources(detailUrl: String) { showLoading(true) val config = configRepo.getConfig() lifecycleScope.launch { try { sources = videoExtractor.extractVideos(detailUrl, config) if (sources.isNotEmpty()) { buildSourceUI() selectSource(0) resetAutoHide() } else { tryPlayDirectly(detailUrl, config) } } catch (e: Exception) { showError("加载失败: ${e.message}") showLoading(false) } } } private fun buildSourceUI() { sourceList.removeAllViews() sources.forEachIndexed { index, source -> val btn = Button(this).apply { text = source.name setBackgroundResource(R.drawable.episode_selector) setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary)) textSize = 13f minWidth = 0 setPadding(16, 8, 16, 8) isFocusable = true isFocusableInTouchMode = true setOnClickListener { selectSource(index) resetAutoHide() } } sourceList.addView(btn) } } private fun selectSource(index: Int) { if (index !in sources.indices) return currentSourceIndex = index for (i in 0 until sourceList.childCount) { sourceList.getChildAt(i).isSelected = (i == index) } val source = sources[index] buildEpisodeUI(source.episodes) if (historyEpisode != null) { scrollToEpisode(historyEpisode!!) } else { if (source.episodes.isNotEmpty()) { playEpisode(source.episodes.first()) } } } private fun buildEpisodeUI(eps: List) { 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) } } private fun scrollToEpisode(episodeTitle: String) { episodeScroll.post { for (i in 0 until episodeList.childCount) { val child = episodeList.getChildAt(i) as? Button if (child?.text == episodeTitle) { val left = child.left - episodeList.paddingStart episodeScroll.smoothScrollTo(left, 0) child.isSelected = true break } } } } private fun playEpisode(ep: Episode) { currentEpisode = ep showLoading(true) val config = configRepo.getConfig() savePlayHistory(ep.title) lifecycleScope.launch { val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(ep.playUrl, config) if (directUrl != null) { playWithExoPlayer(directUrl) } else if (iframeUrl != null) { playWithWebView(iframeUrl) } else { playWithWebView(ep.playUrl) } } } private fun savePlayHistory(episodeName: String?) { lifecycleScope.launch { try { val existing = playHistoryDao.findByDetailUrl(detailUrl) if (existing != null) { playHistoryDao.updatePlayTime(existing.id, System.currentTimeMillis(), episodeName) currentPlayHistoryId = existing.id } else { val playHistory = PlayHistory( title = videoTitle, episodeName = episodeName, detailUrl = detailUrl, coverUrl = coverUrl, category = videoCategory, playTime = System.currentTimeMillis() ) val id = playHistoryDao.insert(playHistory) currentPlayHistoryId = id } } catch (e: Exception) { 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) { savePlayHistory(videoTitle) 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) } } } private fun playWithExoPlayer(url: String) { playerWebView.visibility = View.GONE playerView.visibility = View.VISIBLE showLoading(false) val mediaItem = MediaItem.fromUri(url) exoPlayer?.apply { setMediaItem(mediaItem) prepare() playWhenReady = true if (resumePosition != null && resumePosition!! > 0) { seekTo(resumePosition!!) resumePosition = null } } startPositionSaving() } private fun playWithWebView(url: String) { stopPositionSaving() playerView.visibility = View.GONE playerWebView.visibility = View.VISIBLE showLoading(false) playerWebView.settings.javaScriptEnabled = true playerWebView.settings.domStorageEnabled = true playerWebView.settings.mediaPlaybackRequiresUserGesture = false playerWebView.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) showLoading(false) } } playerWebView.loadUrl(url) } private fun highlightEpisode(view: View) { for (i in 0 until episodeList.childCount) { episodeList.getChildAt(i).isSelected = (episodeList.getChildAt(i) == view) } } private fun showLoading(show: Boolean) { loadingIndicator.visibility = if (show) View.VISIBLE else View.GONE } private fun showError(msg: String) { errorText.text = msg errorText.visibility = View.VISIBLE showLoading(false) } private fun setupTouchListeners() { val touchListener = View.OnTouchListener { view, event -> when (event.action) { 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() { if (controlsVisible) { hideControls() } else { showControls() resetAutoHide() } } 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() { super.onPause() saveCurrentPosition() stopPositionSaving() exoPlayer?.playWhenReady = false } override fun onDestroy() { super.onDestroy() saveCurrentPosition() stopPositionSaving() hideHandler.removeCallbacks(hideRunnable) exoPlayer?.release() exoPlayer = null } }