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 {
|
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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user