refactor: convert from Android TV to phone/tablet mode

- Replace Theme.Leanback with Theme.AppCompat.DayNight.NoActionBar
- Remove leanback dependencies (leanback, leanback-preference)
- Remove LEANBACK_LAUNCHER, leanback feature, banner from manifest
- PlayerActivity: replace D-pad with touch controls (click to toggle episodes, close button)
- SearchFragment: adaptive grid (3 cols phone / 5 cols tablet), remove focus-based history toggle
- Fix deprecated adapterPosition -> bindingAdapterPosition
This commit is contained in:
xiaji
2026-05-24 21:19:34 +08:00
parent 7dee3977de
commit 153b555d52
7 changed files with 34 additions and 54 deletions

View File

@@ -41,10 +41,6 @@ android {
} }
dependencies { dependencies {
// Leanback (TV UI)
implementation("androidx.leanback:leanback:1.0.0")
implementation("androidx.leanback:leanback-preference:1.0.0")
// AndroidX // AndroidX
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.6.1")

View File

@@ -7,9 +7,6 @@
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -20,12 +17,10 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true">
android:banner="@drawable/tv_banner">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
@@ -33,7 +28,6 @@
android:name=".PlayerActivity" android:name=".PlayerActivity"
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/Theme.VideoSearchTV.Player" android:theme="@style/Theme.VideoSearchTV.Player"
android:screenOrientation="landscape"
android:exported="false" /> android:exported="false" />
<activity <activity

View File

@@ -1,11 +1,11 @@
package com.videoapp.tv package com.videoapp.tv
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent
import android.view.View import android.view.View
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.Button import android.widget.Button
import android.widget.ImageButton
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
@@ -15,10 +15,7 @@ import androidx.media3.ui.PlayerView
import com.videoapp.tv.data.ConfigRepository import com.videoapp.tv.data.ConfigRepository
import com.videoapp.tv.engine.Episode import com.videoapp.tv.engine.Episode
import com.videoapp.tv.engine.VideoExtractor import com.videoapp.tv.engine.VideoExtractor
import com.videoapp.tv.engine.WebViewPlayer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PlayerActivity : AppCompatActivity() { class PlayerActivity : AppCompatActivity() {
@@ -28,15 +25,14 @@ class PlayerActivity : AppCompatActivity() {
private lateinit var episodeList: android.widget.LinearLayout private lateinit var episodeList: android.widget.LinearLayout
private lateinit var loadingIndicator: View private lateinit var loadingIndicator: View
private lateinit var errorText: android.widget.TextView private lateinit var errorText: android.widget.TextView
private lateinit var btnClose: ImageButton
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 webViewPlayer by lazy { WebViewPlayer(this) }
private var episodes: List<Episode> = emptyList() private var episodes: List<Episode> = emptyList()
private var currentEpisodeIndex = 0 private var currentEpisodeIndex = 0
private var isPlayerReady = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -48,13 +44,16 @@ class PlayerActivity : AppCompatActivity() {
episodeList = findViewById(R.id.episode_list) episodeList = findViewById(R.id.episode_list)
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)
val detailUrl = intent.getStringExtra("detail_url") ?: "" val detailUrl = intent.getStringExtra("detail_url") ?: ""
val title = intent.getStringExtra("title") ?: "" val title = intent.getStringExtra("title") ?: ""
btnClose.setOnClickListener { finish() }
initExoPlayer() initExoPlayer()
loadVideos(detailUrl, title) loadVideos(detailUrl, title)
setupFullscreenListener() setupTouchListeners()
} }
private fun initExoPlayer() { private fun initExoPlayer() {
@@ -63,7 +62,7 @@ class PlayerActivity : AppCompatActivity() {
} }
} }
private fun loadVideos(detailUrl: String, title: String) { private fun loadVideos(detailUrl: String, @Suppress("UNUSED_PARAMETER") title: String) {
showLoading(true) showLoading(true)
val config = configRepo.getConfig() val config = configRepo.getConfig()
@@ -118,13 +117,10 @@ class PlayerActivity : AppCompatActivity() {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(ep.playUrl, config) val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(ep.playUrl, config)
if (directUrl != null) { if (directUrl != null) {
// Play with ExoPlayer
playWithExoPlayer(directUrl) playWithExoPlayer(directUrl)
} else if (iframeUrl != null) { } else if (iframeUrl != null) {
// Play iframe in WebView
playWithWebView(iframeUrl) playWithWebView(iframeUrl)
} else { } else {
// Fallback: load play page in WebView
playWithWebView(ep.playUrl) playWithWebView(ep.playUrl)
} }
} }
@@ -189,28 +185,15 @@ class PlayerActivity : AppCompatActivity() {
showLoading(false) showLoading(false)
} }
private fun setupFullscreenListener() { private fun setupTouchListeners() {
// Toggle episode panel visibility on dpad up/down
var panelVisible = true var panelVisible = true
playerView.setOnClickListener { playerView.setOnClickListener {
panelVisible = !panelVisible panelVisible = !panelVisible
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
} }
} playerWebView.setOnClickListener {
panelVisible = !panelVisible
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
return when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
episodePanel.visibility = View.VISIBLE
true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (episodePanel.visibility == View.VISIBLE) {
episodePanel.visibility = View.GONE
}
true
}
else -> super.onKeyDown(keyCode, event)
} }
} }

View File

@@ -77,7 +77,8 @@ class SearchFragment : Fragment() {
} }
private fun setupResultsGrid() { private fun setupResultsGrid() {
resultsGrid.layoutManager = GridLayoutManager(context, 4) val spanCount = if (resources.configuration.screenWidthDp >= 600) 5 else 3
resultsGrid.layoutManager = GridLayoutManager(context, spanCount)
resultsGrid.adapter = adapter resultsGrid.adapter = adapter
} }
@@ -107,12 +108,6 @@ class SearchFragment : Fragment() {
showHistory(emptyList()) showHistory(emptyList())
} }
} }
searchInput.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
historyContainer.visibility = View.VISIBLE
}
}
} }
private fun performSearch(keyword: String) { private fun performSearch(keyword: String) {

View File

@@ -41,7 +41,7 @@ class SearchResultAdapter(
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
val pos = adapterPosition val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) { if (pos != RecyclerView.NO_POSITION) {
onItemClick(items[pos]) onItemClick(items[pos])
} }

View File

@@ -18,7 +18,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
<RelativeLayout <ImageButton
android:id="@+id/btn_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="start|top"
android:layout_margin="16dp"
android:background="@drawable/button_bg"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:scaleType="centerInside"
android:contentDescription="@string/back" />
<FrameLayout
android:id="@+id/episode_panel" android:id="@+id/episode_panel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="60dp" android:layout_height="60dp"
@@ -42,7 +53,7 @@
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" /> android:paddingEnd="16dp" />
</HorizontalScrollView> </HorizontalScrollView>
</RelativeLayout> </FrameLayout>
<ProgressBar <ProgressBar
android:id="@+id/loading_indicator" android:id="@+id/loading_indicator"

View File

@@ -1,15 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.VideoSearchTV" parent="Theme.Leanback"> <style name="Theme.VideoSearchTV" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@color/background</item> <item name="android:windowBackground">@color/background</item>
<item name="android:textColor">@color/text_primary</item> <item name="android:textColor">@color/text_primary</item>
<item name="android:colorPrimary">@color/primary</item> <item name="colorPrimary">@color/primary</item>
<item name="android:colorAccent">@color/accent</item> <item name="colorAccent">@color/accent</item>
<item name="android:statusBarColor">@color/background</item>
<item name="android:navigationBarColor">@color/background</item>
</style> </style>
<style name="Theme.VideoSearchTV.Player" parent="Theme.Leanback"> <style name="Theme.VideoSearchTV.Player" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item> <item name="android:statusBarColor">@android:color/black</item>
<item name="android:navigationBarColor">@android:color/black</item> <item name="android:navigationBarColor">@android:color/black</item>