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:
@@ -41,10 +41,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Leanback (TV UI)
|
||||
implementation("androidx.leanback:leanback:1.0.0")
|
||||
implementation("androidx.leanback:leanback-preference:1.0.0")
|
||||
|
||||
// AndroidX
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -20,12 +17,10 @@
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:banner="@drawable/tv_banner">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -33,7 +28,6 @@
|
||||
android:name=".PlayerActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:theme="@style/Theme.VideoSearchTV.Player"
|
||||
android:screenOrientation="landscape"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.videoapp.tv
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -15,10 +15,7 @@ import androidx.media3.ui.PlayerView
|
||||
import com.videoapp.tv.data.ConfigRepository
|
||||
import com.videoapp.tv.engine.Episode
|
||||
import com.videoapp.tv.engine.VideoExtractor
|
||||
import com.videoapp.tv.engine.WebViewPlayer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
@@ -28,15 +25,14 @@ class PlayerActivity : AppCompatActivity() {
|
||||
private lateinit var episodeList: android.widget.LinearLayout
|
||||
private lateinit var loadingIndicator: View
|
||||
private lateinit var errorText: android.widget.TextView
|
||||
private lateinit var btnClose: ImageButton
|
||||
|
||||
private var exoPlayer: ExoPlayer? = null
|
||||
private val videoExtractor = VideoExtractor()
|
||||
private val configRepo by lazy { ConfigRepository(this) }
|
||||
private val webViewPlayer by lazy { WebViewPlayer(this) }
|
||||
|
||||
private var episodes: List<Episode> = emptyList()
|
||||
private var currentEpisodeIndex = 0
|
||||
private var isPlayerReady = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -48,13 +44,16 @@ class PlayerActivity : AppCompatActivity() {
|
||||
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)
|
||||
setupFullscreenListener()
|
||||
setupTouchListeners()
|
||||
}
|
||||
|
||||
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)
|
||||
val config = configRepo.getConfig()
|
||||
|
||||
@@ -118,13 +117,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(ep.playUrl, config)
|
||||
|
||||
if (directUrl != null) {
|
||||
// Play with ExoPlayer
|
||||
playWithExoPlayer(directUrl)
|
||||
} else if (iframeUrl != null) {
|
||||
// Play iframe in WebView
|
||||
playWithWebView(iframeUrl)
|
||||
} else {
|
||||
// Fallback: load play page in WebView
|
||||
playWithWebView(ep.playUrl)
|
||||
}
|
||||
}
|
||||
@@ -189,28 +185,15 @@ class PlayerActivity : AppCompatActivity() {
|
||||
showLoading(false)
|
||||
}
|
||||
|
||||
private fun setupFullscreenListener() {
|
||||
// Toggle episode panel visibility on dpad up/down
|
||||
private fun setupTouchListeners() {
|
||||
var panelVisible = true
|
||||
playerView.setOnClickListener {
|
||||
panelVisible = !panelVisible
|
||||
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
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)
|
||||
playerWebView.setOnClickListener {
|
||||
panelVisible = !panelVisible
|
||||
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ class SearchFragment : Fragment() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -107,12 +108,6 @@ class SearchFragment : Fragment() {
|
||||
showHistory(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
searchInput.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
historyContainer.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performSearch(keyword: String) {
|
||||
|
||||
@@ -41,7 +41,7 @@ class SearchResultAdapter(
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
val pos = adapterPosition
|
||||
val pos = bindingAdapterPosition
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
onItemClick(items[pos])
|
||||
}
|
||||
|
||||
@@ -18,7 +18,18 @@
|
||||
android:layout_height="match_parent"
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
@@ -42,7 +53,7 @@
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp" />
|
||||
</HorizontalScrollView>
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_indicator"
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:textColor">@color/text_primary</item>
|
||||
<item name="android:colorPrimary">@color/primary</item>
|
||||
<item name="android:colorAccent">@color/accent</item>
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="android:statusBarColor">@color/background</item>
|
||||
<item name="android:navigationBarColor">@color/background</item>
|
||||
</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:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
<item name="android:statusBarColor">@android:color/black</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
|
||||
Reference in New Issue
Block a user