Compare commits

...
22 Commits
Author SHA1 Message Date
Li ZongYing 0f4a33a3fc change channel order 2024-01-25 11:37:06 +08:00
Li ZongYing c17d62c698 rename to setting 2024-01-19 13:49:43 +08:00
Li ZongYing 33d3da97e5 fix 13 2024-01-18 22:19:04 +08:00
Li ZongYing ba95ce5374 number selection 2024-01-18 21:42:06 +08:00
Li ZongYing e1ee909071 optimize memu hiding 2024-01-17 10:19:50 +08:00
Li ZongYing 1fd0951f92 channel reversal 2024-01-16 19:04:43 +08:00
Li ZongYing 42b072f5ec support KEYCODE_CHANNEL_UP & KEYCODE_CHANNEL_DOWN 2024-01-14 12:09:12 +08:00
Li ZongYing a23b1e11ce fix the menu did not appear 2024-01-14 10:15:45 +08:00
Li ZongYing 1ac6d87c3c clean log 2024-01-12 10:13:29 +08:00
Li ZongYing b075a7db81 trust all certs 2024-01-11 19:33:31 +08:00
Li ZongYing 7821e8fa58 optimize 2024-01-11 15:19:57 +08:00
Li ZongYing 3109a61bcf action sign 2024-01-10 23:19:21 +08:00
Li ZongYing ce6e5b6546 action sign 2024-01-10 23:18:16 +08:00
Li ZongYing 230e4fea2b action sign 2024-01-10 23:14:49 +08:00
Li ZongYing 0d26912321 action sign 2024-01-10 23:12:35 +08:00
Li ZongYing af9ff5c8dd action sign 2024-01-10 22:39:49 +08:00
Li ZongYing f0f43e0fda dns cache 2024-01-10 22:11:13 +08:00
Li ZongYing 61f62121f2 menu auto close 2024-01-10 18:01:25 +08:00
Li ZongYing d0c7627e35 fix 13 2024-01-08 22:40:56 +08:00
Li ZongYing c10d8b688f add retry 2024-01-08 20:07:53 +08:00
Li ZongYing 0bfac7d679 cctv8k 2024-01-07 21:37:51 +08:00
Li ZongYing c49246f5c7 cctv8k 2024-01-07 21:36:08 +08:00
32 changed files with 910 additions and 364 deletions
+3
View File
@@ -33,6 +33,9 @@ jobs:
signingKeyBase64: ${{ secrets.KEYSTORE }} signingKeyBase64: ${{ secrets.KEYSTORE }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.ALIAS_PASSWORD }} keyPassword: ${{ secrets.ALIAS_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "34.0.0"
- name: Create Release - name: Create Release
id: create_release id: create_release
+24
View File
@@ -14,6 +14,30 @@
## 更新日志 ## 更新日志
### v1.4.7(高版本专用)
* 修复部分用户cctv13播放过程中卡住的问题
* 调整cctv的频道顺序
### v1.4.5(高版本专用)
* 数字选台配置
### v1.4.4(通用版)
* 优化图标显示
* 增加换台反转
### v1.3.3
* 部分错误会提示用户
* 菜单3秒钟后自动关闭
### v1.3.2
* 增加重试,减少因网络问题导致的播放失败
* 优化横幅banner
### v1.2.6 ### v1.2.6
* 支持安卓4.2 * 支持安卓4.2
+6 -3
View File
@@ -100,10 +100,13 @@ dependencies {
// For HLS playback support with ExoPlayer // For HLS playback support with ExoPlayer
implementation "androidx.media3:media3-exoplayer-hls:$media3_version" implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
// 21:2.9.0 17:2.6.4
def retrofit2_version = "2.9.0"
implementation 'com.google.protobuf:protobuf-kotlin:3.25.1' implementation 'com.google.protobuf:protobuf-kotlin:3.25.1'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0' implementation "com.squareup.retrofit2:converter-protobuf:$retrofit2_version"
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation 'androidx.core:core-ktx:1.11.0-beta02' implementation 'androidx.core:core-ktx:1.11.0-beta02'
implementation 'androidx.leanback:leanback:1.2.0-alpha02' implementation 'androidx.leanback:leanback:1.2.0-alpha02'
+2 -3
View File
@@ -9,15 +9,14 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@drawable/logo"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.MyTV"> android:theme="@style/Theme.MyTV">
<activity <activity
android:keepScreenOn="true" android:keepScreenOn="true"
android:name=".MainActivity" android:name=".MainActivity"
android:banner="@drawable/logo" android:banner="@drawable/banner"
android:exported="true" android:exported="true"
android:icon="@drawable/logo" android:icon="@drawable/logo"
android:logo="@drawable/logo" android:logo="@drawable/logo"
Binary file not shown.
Binary file not shown.
@@ -32,10 +32,17 @@ class CardPresenter(
cardView.tag = tvViewModel.videoUrl.value cardView.tag = tvViewModel.videoUrl.value
if (tvViewModel.logo.value != null) { if (tvViewModel.logo.value != null) {
Glide.with(viewHolder.view.context) if (tvViewModel.title.value == "CCTV8K 超高清") {
.load(tvViewModel.logo.value) Glide.with(viewHolder.view.context)
.centerInside() .load(R.drawable.cctv8k)
.into(cardView.mainImageView) .centerInside()
.into(cardView.mainImageView)
} else {
Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
.into(cardView.mainImageView)
}
cardView.setBackgroundColor(Color.WHITE) cardView.setBackgroundColor(Color.WHITE)
cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE) cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE)
@@ -0,0 +1,74 @@
package com.lizongying.mytv
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.lizongying.mytv.databinding.ChannelBinding
import com.lizongying.mytv.models.TVViewModel
class ChannelFragment : Fragment() {
private var _binding: ChannelBinding? = null
private val binding get() = _binding!!
private val handler = Handler()
private val delay: Long = 3000
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = ChannelBinding.inflate(inflater, container, false)
_binding!!.root.visibility = View.GONE
(activity as MainActivity).fragmentReady()
return binding.root
}
fun show(tvViewModel: TVViewModel) {
binding.channelContent.text = tvViewModel.id.value.toString()
handler.removeCallbacks(removeRunnable)
view?.visibility = View.VISIBLE
handler.postDelayed(removeRunnable, delay)
}
fun show(channel: String) {
if (binding.channelContent.text == "") {
binding.channelContent.text = channel
handler.removeCallbacks(removeRunnable)
view?.visibility = View.VISIBLE
handler.postDelayed(removeRunnable, delay)
} else {
val ch = "${binding.channelContent.text}$channel".toInt()
(activity as MainActivity).play(ch)
binding.channelContent.text = ""
view?.visibility = View.GONE
}
}
override fun onResume() {
super.onResume()
handler.postDelayed(removeRunnable, delay)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(removeRunnable)
}
private val removeRunnable = Runnable {
binding.channelContent.text = ""
view?.visibility = View.GONE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "ChannelFragment"
}
}
@@ -23,22 +23,23 @@ class InfoFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
_binding = InfoBinding.inflate(inflater, container, false) _binding = InfoBinding.inflate(inflater, container, false)
_binding!!.root.visibility = View.GONE
(activity as MainActivity).fragmentReady() (activity as MainActivity).fragmentReady()
return binding.root return binding.root
} }
override fun onResume() {
super.onResume()
// Use a Handler to delay the fragment transaction
handler.postDelayed(removeRunnable, delay)
}
fun show(tvViewModel: TVViewModel) { fun show(tvViewModel: TVViewModel) {
binding.textView.text = tvViewModel.title.value binding.textView.text = tvViewModel.title.value
Glide.with(this) if (tvViewModel.title.value == "CCTV8K 超高清") {
.load(tvViewModel.logo.value) Glide.with(this)
.into(binding.infoLogo) .load(R.drawable.cctv8k)
.into(binding.infoLogo)
} else {
Glide.with(this)
.load(tvViewModel.logo.value)
.into(binding.infoLogo)
}
val program = tvViewModel.getProgramOne() val program = tvViewModel.getProgramOne()
if (program != null) { if (program != null) {
binding.infoDesc.text = program.name binding.infoDesc.text = program.name
@@ -49,10 +50,13 @@ class InfoFragment : Fragment() {
handler.postDelayed(removeRunnable, delay) handler.postDelayed(removeRunnable, delay)
} }
override fun onResume() {
super.onResume()
handler.postDelayed(removeRunnable, delay)
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
Log.i(TAG, "onPause")
// Cancel the delayed task when the fragment is paused
handler.removeCallbacks(removeRunnable) handler.removeCallbacks(removeRunnable)
} }
@@ -1,6 +1,7 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.app.AlertDialog import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.Signature import android.content.pm.Signature
@@ -15,11 +16,7 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.models.TVViewModel
import java.security.MessageDigest import java.security.MessageDigest
@@ -30,11 +27,23 @@ class MainActivity : FragmentActivity() {
var playerFragment = PlayerFragment() var playerFragment = PlayerFragment()
private val mainFragment = MainFragment() private val mainFragment = MainFragment()
private val infoFragment = InfoFragment() private val infoFragment = InfoFragment()
private val channelFragment = ChannelFragment()
private lateinit var settingFragment: SettingFragment
private var doubleBackToExitPressedOnce = false private var doubleBackToExitPressedOnce = false
private lateinit var gestureDetector: GestureDetector private lateinit var gestureDetector: GestureDetector
private val handler = Handler()
private val delay: Long = 4000
private val delayHideHelp: Long = 10000
private lateinit var sharedPref: SharedPreferences
private var channelReversal = false
private var channelNum = true
private var versionName = ""
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@@ -47,24 +56,52 @@ class MainActivity : FragmentActivity() {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.main_browse_fragment, playerFragment) .add(R.id.main_browse_fragment, playerFragment)
.add(R.id.main_browse_fragment, infoFragment) .add(R.id.main_browse_fragment, infoFragment)
.add(R.id.main_browse_fragment, channelFragment)
.add(R.id.main_browse_fragment, mainFragment) .add(R.id.main_browse_fragment, mainFragment)
.hide(infoFragment) .hide(mainFragment)
.commit() .commit()
mainFragment.view?.requestFocus() mainFragment.view?.requestFocus()
} }
gestureDetector = GestureDetector(this, GestureListener()) gestureDetector = GestureDetector(this, GestureListener())
sharedPref = getPreferences(Context.MODE_PRIVATE)
channelReversal = sharedPref.getBoolean(CHANNEL_REVERSAL, channelReversal)
channelNum = sharedPref.getBoolean(CHANNEL_NUM, channelNum)
versionName = getPackageInfo().versionName
settingFragment = SettingFragment(versionName, channelReversal, channelNum)
} }
fun showInfoFragment(tvViewModel: TVViewModel) { fun showInfoFragment(tvViewModel: TVViewModel) {
infoFragment.show(tvViewModel) infoFragment.show(tvViewModel)
if (channelNum) {
channelFragment.show(tvViewModel)
}
}
private fun showChannel(channel: String) {
if (!mainFragment.isHidden) {
return
}
if (settingFragment.isVisible) {
return
}
if (channelNum) {
channelFragment.show(channel)
}
} }
fun play(tvViewModel: TVViewModel) { fun play(tvViewModel: TVViewModel) {
Log.i(TAG, "play: ${tvViewModel.getTV()}")
playerFragment.play(tvViewModel) playerFragment.play(tvViewModel)
mainFragment.view?.requestFocus() mainFragment.view?.requestFocus()
} }
fun play(itemPosition: Int) {
mainFragment.play(itemPosition)
}
fun prev() { fun prev() {
mainFragment.prev() mainFragment.prev()
} }
@@ -74,11 +111,11 @@ class MainActivity : FragmentActivity() {
} }
private fun prevSource() { private fun prevSource() {
mainFragment.prevSource() // mainFragment.prevSource()
} }
private fun nextSource() { private fun nextSource() {
mainFragment.nextSource() // mainFragment.nextSource()
} }
fun switchMainFragment() { fun switchMainFragment() {
@@ -86,6 +123,7 @@ class MainActivity : FragmentActivity() {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
transaction.show(mainFragment) transaction.show(mainFragment)
keepRunnable()
} else { } else {
transaction.hide(mainFragment) transaction.hide(mainFragment)
} }
@@ -93,6 +131,17 @@ class MainActivity : FragmentActivity() {
transaction.commit() transaction.commit()
} }
fun keepRunnable() {
handler.removeCallbacks(hideRunnable)
handler.postDelayed(hideRunnable, delay)
}
private val hideRunnable = Runnable {
if (!mainFragment.isHidden) {
supportFragmentManager.beginTransaction().hide(mainFragment).commit()
}
}
private fun mainFragmentIsHidden(): Boolean { private fun mainFragmentIsHidden(): Boolean {
return mainFragment.isHidden return mainFragment.isHidden
} }
@@ -157,88 +206,205 @@ class MainActivity : FragmentActivity() {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { fun saveChannelReversal(channelReversal: Boolean) {
when (keyCode) { with(sharedPref.edit()) {
KeyEvent.KEYCODE_BACK -> { putBoolean(CHANNEL_REVERSAL, channelReversal)
if (!mainFragmentIsHidden()) { apply()
hideMainFragment() }
return true this.channelReversal = channelReversal
} }
if (doubleBackToExitPressedOnce) { fun saveChannelNum(channelNum: Boolean) {
super.onBackPressed() with(sharedPref.edit()) {
return true putBoolean(CHANNEL_NUM, channelNum)
} apply()
}
this.channelNum = channelNum
}
this.doubleBackToExitPressedOnce = true private fun showHelp() {
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show() if (!mainFragment.isHidden) {
return
}
Handler(Looper.getMainLooper()).postDelayed({ Log.i(TAG, "settingFragment ${settingFragment.isVisible}")
doubleBackToExitPressedOnce = false if (!settingFragment.isVisible) {
}, 2000) settingFragment.show(supportFragmentManager, "setting")
return true handler.removeCallbacks(hideHelp)
handler.postDelayed(hideHelp, delayHideHelp)
} else {
handler.removeCallbacks(hideHelp)
settingFragment.dismiss()
}
}
private val hideHelp = Runnable {
if (settingFragment.isVisible) {
settingFragment.dismiss()
}
}
private fun channelUp() {
if (mainFragment.isHidden) {
if (channelReversal) {
next()
return
} }
prev()
KeyEvent.KEYCODE_MENU -> { } else {
val versionName = getPackageInfo().versionName
val textView = TextView(this)
textView.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
val imageView = ImageView(this)
val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate)
imageView.setImageDrawable(drawable)
val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
linearLayout.addView(textView)
linearLayout.addView(imageView)
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
imageView.layoutParams = layoutParams
textView.layoutParams = layoutParams
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder
.setView(linearLayout)
val dialog: AlertDialog = builder.create()
dialog.show()
return true
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
Log.i(TAG, "KEYCODE_DPAD_CENTER")
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (mainFragment.isHidden) {
prev()
} else {
// if (mainFragment.selectedPosition == 0) { // if (mainFragment.selectedPosition == 0) {
// mainFragment.setSelectedPosition( // mainFragment.setSelectedPosition(
// mainFragment.tvListViewModel.maxNum.size - 1, // mainFragment.tvListViewModel.maxNum.size - 1,
// false // false
// ) // )
// } // }
} }
} }
KeyEvent.KEYCODE_DPAD_DOWN -> { private fun channelDown() {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
next() if (channelReversal) {
} else { prev()
return
}
next()
} else {
// if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) { // if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) {
//// mainFragment.setSelectedPosition(0, false) //// mainFragment.setSelectedPosition(0, false)
// hideMainFragment() // hideMainFragment()
// return false // return false
// } // }
} }
}
private fun back() {
if (!mainFragmentIsHidden()) {
hideMainFragment()
return
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return
}
doubleBackToExitPressedOnce = true
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show()
Handler(Looper.getMainLooper()).postDelayed({
doubleBackToExitPressedOnce = false
}, 2000)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_0 -> {
showChannel("0")
return true
}
KeyEvent.KEYCODE_1 -> {
showChannel("1")
return true
}
KeyEvent.KEYCODE_2 -> {
showChannel("2")
return true
}
KeyEvent.KEYCODE_3 -> {
showChannel("3")
return true
}
KeyEvent.KEYCODE_4 -> {
showChannel("4")
return true
}
KeyEvent.KEYCODE_5 -> {
showChannel("5")
return true
}
KeyEvent.KEYCODE_6 -> {
showChannel("6")
return true
}
KeyEvent.KEYCODE_7 -> {
showChannel("7")
return true
}
KeyEvent.KEYCODE_8 -> {
showChannel("8")
return true
}
KeyEvent.KEYCODE_9 -> {
showChannel("9")
return true
}
KeyEvent.KEYCODE_ESCAPE -> {
back()
return true
}
KeyEvent.KEYCODE_BACK -> {
back()
return true
}
KeyEvent.KEYCODE_BOOKMARK -> {
showHelp()
return true
}
KeyEvent.KEYCODE_UNKNOWN -> {
showHelp()
return true
}
KeyEvent.KEYCODE_HELP -> {
showHelp()
return true
}
KeyEvent.KEYCODE_SETTINGS -> {
showHelp()
return true
}
KeyEvent.KEYCODE_MENU -> {
showHelp()
return true
}
KeyEvent.KEYCODE_ENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
channelUp()
}
KeyEvent.KEYCODE_CHANNEL_UP -> {
channelUp()
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
channelDown()
}
KeyEvent.KEYCODE_CHANNEL_DOWN -> {
channelDown()
} }
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
@@ -323,5 +489,7 @@ class MainActivity : FragmentActivity() {
companion object { companion object {
private const val TAG = "MainActivity" private const val TAG = "MainActivity"
private const val CHANNEL_REVERSAL = "channel_reversal"
private const val CHANNEL_NUM = "channel_num"
} }
} }
@@ -6,6 +6,7 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.app.BrowseSupportFragment
import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.HeaderItem
@@ -24,19 +25,19 @@ import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainFragment : BrowseSupportFragment() { class MainFragment : BrowseSupportFragment() {
var itemPosition: Int = 0
private var request: Request = Request() private var itemPosition = 0
private var rowsAdapter: ArrayObjectAdapter? = null private var rowsAdapter: ArrayObjectAdapter? = null
private var request = Request()
var tvListViewModel = TVListViewModel() var tvListViewModel = TVListViewModel()
private var sharedPref: SharedPreferences? = null private var sharedPref: SharedPreferences? = null
private var lastVideoUrl: String = "" private var lastVideoUrl = ""
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable
@@ -52,7 +53,6 @@ class MainFragment : BrowseSupportFragment() {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
activity?.let { request.initYSP(it) } activity?.let { request.initYSP(it) }
sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)
loadRows() loadRows()
@@ -63,6 +63,13 @@ class MainFragment : BrowseSupportFragment() {
handler.post(mUpdateProgramRunnable) handler.post(mUpdateProgramRunnable)
tvListViewModel.getTVListViewModel().value?.forEach { tvViewModel -> tvListViewModel.getTVListViewModel().value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.id.value == itemPosition
) {
Toast.makeText(context, tvViewModel.errInfo.value, Toast.LENGTH_SHORT).show()
}
}
tvViewModel.ready.observe(viewLifecycleOwner) { _ -> tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
// not first time && channel not change // not first time && channel not change
@@ -78,8 +85,8 @@ class MainFragment : BrowseSupportFragment() {
if (tvViewModel.change.value != null) { if (tvViewModel.change.value != null) {
val title = tvViewModel.title.value val title = tvViewModel.title.value
Log.i(TAG, "switch $title") Log.i(TAG, "switch $title")
if (tvViewModel.pid.value != null) { if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title") Log.i(TAG, "request $title ${tvViewModel.pid.value}")
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request.fetchData(it) } tvViewModel.let { request.fetchData(it) }
} }
@@ -92,7 +99,6 @@ class MainFragment : BrowseSupportFragment() {
if (check(tvViewModel)) { if (check(tvViewModel)) {
(activity as? MainActivity)?.play(tvViewModel) (activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel) (activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition( setSelectedPosition(
tvViewModel.getRowPosition(), true, tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition()) SelectItemViewHolderTask(tvViewModel.getItemPosition())
@@ -106,58 +112,6 @@ class MainFragment : BrowseSupportFragment() {
fragmentReady() fragmentReady()
} }
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(mUpdateProgramRunnable)
with(sharedPref!!.edit()) {
putInt("position", itemPosition)
apply()
}
}
fun updateProgram(tvViewModel: TVViewModel) {
val timestamp = getDateTimestamp()
if (timestamp - tvViewModel.programUpdateTime > 60) {
if (tvViewModel.program.value!!.isEmpty()) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
} else {
if (timestamp - tvViewModel.program.value!!.last().et < 600) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
}
}
}
}
inner class UpdateProgramRunnable : Runnable {
override fun run() {
tvListViewModel.getTVListViewModel().value?.filter { it.programId.value != null }
?.forEach { tvViewModel ->
updateProgram(
tvViewModel
)
}
handler.postDelayed(this, 60000)
}
}
fun check(tvViewModel: TVViewModel): Boolean {
val title = tvViewModel.title.value
val videoUrl = tvViewModel.videoIndex.value?.let { tvViewModel.videoUrl.value?.get(it) }
if (videoUrl == null || videoUrl == "") {
Log.e(TAG, "$title videoUrl is empty")
return false
}
if (videoUrl == lastVideoUrl) {
Log.e(TAG, "$title videoUrl is duplication")
return false
}
return true
}
fun toLastPosition() { fun toLastPosition() {
setSelectedPosition( setSelectedPosition(
selectedPosition, false, selectedPosition, false,
@@ -198,48 +152,11 @@ class MainFragment : BrowseSupportFragment() {
adapter = rowsAdapter adapter = rowsAdapter
itemPosition = sharedPref?.getInt("position", 0)!! itemPosition = sharedPref?.getInt(POSITION, 0)!!
if (itemPosition >= tvListViewModel.size()) { if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0 itemPosition = 0
tvListViewModel.setItemPosition(itemPosition)
}
}
fun fragmentReady() {
ready++
Log.i(TAG, "ready $ready")
if (ready == 3) {
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
(activity as? MainActivity)?.switchMainFragment()
}
}
fun prev() {
view?.post {
itemPosition--
if (itemPosition == -1) {
itemPosition = tvListViewModel.size() - 1
}
tvListViewModel.setItemPosition(itemPosition)
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
}
}
fun next() {
view?.post {
itemPosition++
if (itemPosition == tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
} }
tvListViewModel.setItemPosition(itemPosition)
} }
fun prevSource() { fun prevSource() {
@@ -288,9 +205,7 @@ class MainFragment : BrowseSupportFragment() {
if (itemPosition != item.id.value!!) { if (itemPosition != item.id.value!!) {
itemPosition = item.id.value!! itemPosition = item.id.value!!
tvListViewModel.setItemPosition(itemPosition) tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
} }
(activity as? MainActivity)?.switchMainFragment() (activity as? MainActivity)?.switchMainFragment()
} }
@@ -304,16 +219,118 @@ class MainFragment : BrowseSupportFragment() {
) { ) {
if (item is TVViewModel) { if (item is TVViewModel) {
tvListViewModel.setItemPositionCurrent(item.id.value!!) tvListViewModel.setItemPositionCurrent(item.id.value!!)
(activity as MainActivity).keepRunnable()
} }
} }
} }
fun check(tvViewModel: TVViewModel): Boolean {
val title = tvViewModel.title.value
val videoUrl = tvViewModel.videoIndex.value?.let { tvViewModel.videoUrl.value?.get(it) }
if (videoUrl == null || videoUrl == "") {
Log.e(TAG, "$title videoUrl is empty")
return false
}
if (videoUrl == lastVideoUrl) {
Log.e(TAG, "$title videoUrl is duplication")
return false
}
return true
}
fun fragmentReady() {
ready++
Log.i(TAG, "ready $ready")
if (ready == 4) {
// request.fetchPage()
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
fun play(itemPosition: Int) {
view?.post {
if (itemPosition < tvListViewModel.size()) {
this.itemPosition = itemPosition
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
}
fun prev() {
view?.post {
itemPosition--
if (itemPosition == -1) {
itemPosition = tvListViewModel.size() - 1
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
fun next() {
view?.post {
itemPosition++
if (itemPosition == tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
fun updateProgram(tvViewModel: TVViewModel) {
val timestamp = getDateTimestamp()
if (timestamp - tvViewModel.programUpdateTime > 60) {
if (tvViewModel.program.value!!.isEmpty()) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
} else {
if (timestamp - tvViewModel.program.value!!.last().et < 600) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
}
}
}
}
inner class UpdateProgramRunnable : Runnable {
override fun run() {
tvListViewModel.getTVListViewModel().value?.filter { it.programId.value != null }
?.forEach { tvViewModel ->
updateProgram(
tvViewModel
)
}
handler.postDelayed(this, 60000)
}
}
override fun onStop() {
Log.i(TAG, "onStop")
super.onStop()
with(sharedPref!!.edit()) {
putInt(POSITION, itemPosition)
apply()
}
Log.i(TAG, "POSITION saved")
}
override fun onDestroy() {
Log.i(TAG, "onDestroy")
super.onDestroy()
handler.removeCallbacks(mUpdateProgramRunnable)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
view!!.requestFocus() view?.post { view?.requestFocus() }
} }
companion object { companion object {
private const val TAG = "MainFragment" private const val TAG = "MainFragment"
private const val POSITION = "position"
} }
} }
@@ -24,6 +24,7 @@ class PlayerFragment : Fragment() {
private var _binding: PlayerBinding? = null private var _binding: PlayerBinding? = null
private var playerView: PlayerView? = null private var playerView: PlayerView? = null
private var tvViewModel: TVViewModel? = null private var tvViewModel: TVViewModel? = null
private val aspectRatio = 16f / 9f
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@@ -43,10 +44,9 @@ class PlayerFragment : Fragment() {
playerView!!.player?.playWhenReady = true playerView!!.player?.playWhenReady = true
playerView!!.player?.addListener(object : Player.Listener { playerView!!.player?.addListener(object : Player.Listener {
override fun onVideoSizeChanged(videoSize: VideoSize) { override fun onVideoSizeChanged(videoSize: VideoSize) {
val aspectRatio = 16f / 9f
val layoutParams = playerView?.layoutParams
val ratio = playerView?.measuredWidth?.div(playerView?.measuredHeight!!) val ratio = playerView?.measuredWidth?.div(playerView?.measuredHeight!!)
if (ratio != null) { if (ratio != null) {
val layoutParams = playerView?.layoutParams
if (ratio < aspectRatio) { if (ratio < aspectRatio) {
layoutParams?.height = layoutParams?.height =
(playerView?.measuredWidth?.div(aspectRatio))?.toInt() (playerView?.measuredWidth?.div(aspectRatio))?.toInt()
@@ -74,10 +74,9 @@ class PlayerFragment : Fragment() {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun play(tvViewModel: TVViewModel) { fun play(tvViewModel: TVViewModel) {
this.tvViewModel = tvViewModel this.tvViewModel = tvViewModel
val videoUrlCurrent = val videoUrlCurrent = tvViewModel.getVideoUrlCurrent()
tvViewModel.videoIndex.value?.let { tvViewModel.videoUrl.value?.get(it) }
playerView?.player?.run { playerView?.player?.run {
videoUrlCurrent?.let { setMediaItem(MediaItem.fromUri(it)) } setMediaItem(MediaItem.fromUri(videoUrlCurrent))
prepare() prepare()
} }
} }
@@ -103,6 +102,11 @@ class PlayerFragment : Fragment() {
} }
} }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object { companion object {
private const val TAG = "PlaybackVideoFragment" private const val TAG = "PlaybackVideoFragment"
} }
+93 -108
View File
@@ -7,11 +7,9 @@ import android.util.Base64
import android.util.Log import android.util.Log
import com.lizongying.mytv.Utils.getDateFormat import com.lizongying.mytv.Utils.getDateFormat
import com.lizongying.mytv.api.ApiClient import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.BtraceClient
import com.lizongying.mytv.api.Info import com.lizongying.mytv.api.Info
import com.lizongying.mytv.api.LiveInfo import com.lizongying.mytv.api.LiveInfo
import com.lizongying.mytv.api.LiveInfoRequest import com.lizongying.mytv.api.LiveInfoRequest
import com.lizongying.mytv.api.ProtoClient
import com.lizongying.mytv.api.YSP import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.api.YSPApiService import com.lizongying.mytv.api.YSPApiService
import com.lizongying.mytv.api.YSPBtraceService import com.lizongying.mytv.api.YSPBtraceService
@@ -26,14 +24,13 @@ import retrofit2.Response
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.reflect.KFunction0
class Request { class Request {
private var yspTokenService: YSPTokenService = ApiClient().yspTokenService private var yspTokenService: YSPTokenService = ApiClient().yspTokenService
private var yspApiService: YSPApiService = ApiClient().yspApiService private var yspApiService: YSPApiService = ApiClient().yspApiService
private var yspBtraceService: YSPBtraceService = BtraceClient().yspBtraceService private var yspBtraceService: YSPBtraceService = ApiClient().yspBtraceService
private var yspProtoService: YSPProtoService = ProtoClient().yspProtoService private var yspProtoService: YSPProtoService = ApiClient().yspProtoService
private var ysp: YSP? = null private var ysp: YSP? = null
// TODO onDestroy // TODO onDestroy
@@ -63,7 +60,6 @@ class Request {
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道", "CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道", "CGTN西班牙语频道" to "CGTN 西班牙语频道",
"CGTN外语纪录频道" to "CGTN 纪录频道", "CGTN外语纪录频道" to "CGTN 纪录频道",
"东方卫视" to "东方卫视", "东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视", "湖南卫视" to "湖南卫视",
"湖北卫视" to "湖北卫视", "湖北卫视" to "湖北卫视",
@@ -87,34 +83,14 @@ class Request {
"海南卫视" to "海南卫视", "海南卫视" to "海南卫视",
) )
private var token: String? = null
fun initYSP(context: Context) { fun initYSP(context: Context) {
ysp = YSP(context) ysp = YSP(context)
} }
fun fetchToken(fragmentReady: KFunction0<Unit>) { var call: Call<LiveInfo>? = null
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
val info = response.body()
token = info?.data?.token
Log.i(TAG, "info success $token")
} else {
Log.e(TAG, "info status error")
}
fragmentReady()
}
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
fragmentReady()
}
})
}
fun fetchVideo(tvModel: TVViewModel, cookie: String) { fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel()
if (::myRunnable.isInitialized) { if (::myRunnable.isInitialized) {
handler.removeCallbacks(myRunnable) handler.removeCallbacks(myRunnable)
} }
@@ -124,97 +100,107 @@ class Request {
tvModel.seq = 0 tvModel.seq = 0
val data = ysp?.switch(tvModel) val data = ysp?.switch(tvModel)
val request = data?.let { LiveInfoRequest(it) } val request = data?.let { LiveInfoRequest(it) }
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", it) }
request?.let { yspApiService.getLiveInfo(cookie, it) } call?.enqueue(object : Callback<LiveInfo> {
?.enqueue(object : Callback<LiveInfo> { override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) { if (response.isSuccessful) {
if (response.isSuccessful) { val liveInfo = response.body()
val liveInfo = response.body() if (liveInfo?.data?.playurl != null) {
if (liveInfo?.data?.playurl != null) { val chanll = liveInfo.data.chanll
val chanll = liveInfo.data.chanll val decodedBytes = Base64.decode(
val decodedBytes = Base64.decode( chanll.substring(9, chanll.length - 3),
chanll.substring(9, chanll.length - 3), Base64.DEFAULT
Base64.DEFAULT )
) val decodedString = String(decodedBytes)
val decodedString = String(decodedBytes) val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""") val matchResult = regex.find(decodedString)
val matchResult = regex.find(decodedString) if (matchResult != null) {
if (matchResult != null) { val (key, iv) = matchResult.destructured
val (key, iv) = matchResult.destructured val keyBytes = Base64.decode(key, Base64.DEFAULT)
val keyBytes = Base64.decode(key, Base64.DEFAULT) val ivBytes = Base64.decode(iv, Base64.DEFAULT)
val ivBytes = Base64.decode(iv, Base64.DEFAULT) val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES( keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0), ivBytes
ivBytes ).uppercase()
).uppercase() Log.i(TAG, "$title url $url")
Log.i(TAG, "$title url $url") tvModel.addVideoUrl(url)
tvModel.addVideoUrl(url) tvModel.allReady()
tvModel.allReady() tvModel.retryTimes = 0
myRunnable = MyRunnable(tvModel)
myRunnable = MyRunnable(tvModel) handler.post(myRunnable)
handler.post(myRunnable)
} else {
Log.e(TAG, "$title key error")
tvModel.firstSource()
}
} else { } else {
Log.e(TAG, "$title url error $request") Log.e(TAG, "$title key error")
tvModel.firstSource() if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
} }
} else { } else {
Log.e(TAG, "$title status error") if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") {
tvModel.firstSource() Log.e(TAG, "$title error ${liveInfo.data.errinfo}")
tvModel.setErrInfo(liveInfo.data.errinfo)
} else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
}
} else {
Log.e(TAG, "$title status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
}
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
})
}
fun fetchVideo(tvModel: TVViewModel) {
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
val token = response.body()?.data?.token
Log.i(TAG, "info success $token")
val cookie =
"vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel)
}
} }
} }
override fun onFailure(call: Call<LiveInfo>, t: Throwable) { override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "$title request error") Log.e(TAG, "info request error $t")
tvModel.firstSource() if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel)
}
} }
}) })
} }
fun fetchData(tvModel: TVViewModel) { fun fetchData(tvModel: TVViewModel) {
var cookie = "guid=1; vplatform=109" if (tvModel.needToken) {
val channels = arrayOf( fetchVideo(tvModel)
"CCTV3 综艺",
"CCTV6 电影",
"CCTV8 电视剧",
"风云剧场",
"第一剧场",
"怀旧剧场",
"世界地理",
"风云音乐",
"兵器科技",
"风云足球",
"高尔夫网球",
"女性时尚",
"央视文化精品",
"央视台球",
"电视指南",
"卫生健康",
)
if (tvModel.title.value in channels) {
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
val info = response.body()
token = info?.data?.token
Log.i(TAG, "info success $token")
cookie =
"guid=1; vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
}
}
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
}
})
} else { } else {
val cookie = "vplatform=109"
fetchVideo(tvModel, cookie) fetchVideo(tvModel, cookie)
} }
} }
@@ -251,7 +237,6 @@ class Request {
// Log.d(TAG, "$title kvcollect success") // Log.d(TAG, "$title kvcollect success")
} else { } else {
Log.e(TAG, "$title kvcollect status error") Log.e(TAG, "$title kvcollect status error")
tvModel.firstSource()
} }
} }
@@ -278,7 +263,7 @@ class Request {
} }
Log.i( Log.i(
TAG, TAG,
"${item.channelName} ,${item.tvLogo},${item.pid},${item.streamId}" "${item.channelName},${item.pid},${item.streamId}"
) )
var channelType = "央视频道" var channelType = "央视频道"
if (item?.channelType === "weishi") { if (item?.channelType === "weishi") {
@@ -0,0 +1,58 @@
package com.lizongying.mytv
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.lizongying.mytv.databinding.DialogBinding
class SettingFragment(private val versionName: String,
private val channelReversal: Boolean,
private val channelNum: Boolean,
) :
DialogFragment() {
private var _binding: DialogBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogBinding.inflate(inflater, container, false)
_binding?.version?.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
val switchChannelReversal = _binding?.switchChannelReversal
switchChannelReversal?.isChecked = channelReversal
switchChannelReversal?.setOnCheckedChangeListener { _, isChecked ->
(activity as MainActivity).saveChannelReversal(isChecked)
}
val switchChannelNum = _binding?.switchChannelNum
switchChannelNum?.isChecked = channelNum
switchChannelNum?.setOnCheckedChangeListener { _, isChecked ->
(activity as MainActivity).saveChannelNum(isChecked)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "SettingFragment"
}
}
@@ -7,6 +7,7 @@ object TVList {
private var mappingLogo = mapOf( private var mappingLogo = mapOf(
"CCTV4K 超高清" to "https://resources.yangshipin.cn/assets/oms/image/202306/3e9d06fd7244d950df5838750f1c6ac3456e172b51caca2c16d2282125b111e8.png?imageMogr2/format/webp", "CCTV4K 超高清" to "https://resources.yangshipin.cn/assets/oms/image/202306/3e9d06fd7244d950df5838750f1c6ac3456e172b51caca2c16d2282125b111e8.png?imageMogr2/format/webp",
"CCTV8K 超高清" to "https://static.wikia.nocookie.net/logos/images/6/69/CCTV8K.png/revision/latest/scale-to-width-down/120?cb=20230104110835&path-prefix=vi",
"CCTV1 综合" to "https://resources.yangshipin.cn/assets/oms/image/202306/d57905b93540bd15f0c48230dbbbff7ee0d645ff539e38866e2d15c8b9f7dfcd.png?imageMogr2/format/webp", "CCTV1 综合" to "https://resources.yangshipin.cn/assets/oms/image/202306/d57905b93540bd15f0c48230dbbbff7ee0d645ff539e38866e2d15c8b9f7dfcd.png?imageMogr2/format/webp",
"CCTV2 财经" to "https://resources.yangshipin.cn/assets/oms/image/202306/20115388de0207131af17eac86c33049b95d69eaff064e55653a1b941810a006.png?imageMogr2/format/webp", "CCTV2 财经" to "https://resources.yangshipin.cn/assets/oms/image/202306/20115388de0207131af17eac86c33049b95d69eaff064e55653a1b941810a006.png?imageMogr2/format/webp",
"CCTV3 综艺" to "https://resources.yangshipin.cn/assets/oms/image/202306/7b7a65c712450da3deb6ca66fbacf4f9aee00d3f20bd80eafb5ada01ec63eb3a.png?imageMogr2/format/webp", "CCTV3 综艺" to "https://resources.yangshipin.cn/assets/oms/image/202306/7b7a65c712450da3deb6ca66fbacf4f9aee00d3f20bd80eafb5ada01ec63eb3a.png?imageMogr2/format/webp",
@@ -68,6 +69,7 @@ object TVList {
) )
private var mappingEPG = mapOf( private var mappingEPG = mapOf(
"CCTV4K 超高清" to "600002264", "CCTV4K 超高清" to "600002264",
"CCTV8K 超高清" to "600156816",
"CCTV1 综合" to "600001859", "CCTV1 综合" to "600001859",
"CCTV2 财经" to "600001800", "CCTV2 财经" to "600001800",
"CCTV3 综艺" to "600001801", "CCTV3 综艺" to "600001801",
@@ -129,6 +131,7 @@ object TVList {
) )
private var mappingVideo = mapOf( private var mappingVideo = mapOf(
"CCTV4K 超高清" to arrayOf("600002264", "2000266303"), "CCTV4K 超高清" to arrayOf("600002264", "2000266303"),
"CCTV8K 超高清" to arrayOf("600156816", "2020603421"),
"CCTV1 综合" to arrayOf("600001859", "2000210103"), "CCTV1 综合" to arrayOf("600001859", "2000210103"),
"CCTV2 财经" to arrayOf("600001800", "2000203603"), "CCTV2 财经" to arrayOf("600001800", "2000203603"),
"CCTV3 综艺" to arrayOf("600001801", "2000203803"), "CCTV3 综艺" to arrayOf("600001801", "2000203803"),
@@ -142,7 +145,7 @@ object TVList {
"CCTV10 科教" to arrayOf("600001805", "2000203503"), "CCTV10 科教" to arrayOf("600001805", "2000203503"),
"CCTV11 戏曲" to arrayOf("600001806", "2000204103"), "CCTV11 戏曲" to arrayOf("600001806", "2000204103"),
"CCTV12 社会与法" to arrayOf("600001807", "2000202603"), "CCTV12 社会与法" to arrayOf("600001807", "2000202603"),
// "CCTV13 新闻" to arrayOf("600001811","2000204603"), "CCTV13 新闻" to arrayOf("600001811","2000204603"),
"CCTV14 少儿" to arrayOf("600001809", "2000204403"), "CCTV14 少儿" to arrayOf("600001809", "2000204403"),
"CCTV15 音乐" to arrayOf("600001815", "2000205003"), "CCTV15 音乐" to arrayOf("600001815", "2000205003"),
"CCTV16 奥林匹克" to arrayOf("600098637", "2012375003"), "CCTV16 奥林匹克" to arrayOf("600098637", "2012375003"),
@@ -193,7 +196,6 @@ object TVList {
private fun setupTV(): Map<String, List<TV>> { private fun setupTV(): Map<String, List<TV>> {
val tvs = """ val tvs = """
央视频道 央视频道
CCTV4K 超高清,
CCTV1 综合,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226231/index.m3u8 CCTV1 综合,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226231/index.m3u8
CCTV2 财经,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226195/index.m3u8 CCTV2 财经,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226195/index.m3u8
CCTV3 综艺,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226397/index.m3u8 CCTV3 综艺,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226397/index.m3u8
@@ -212,12 +214,8 @@ CCTV14 少儿,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226193/inde
CCTV15 音乐,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8 CCTV15 音乐,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8
CCTV16 奥林匹克,http://39.134.24.162/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226921/index.m3u8;http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226921/index.m3u8 CCTV16 奥林匹克,http://39.134.24.162/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226921/index.m3u8;http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226921/index.m3u8
CCTV17 农业农村,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226198/index.m3u8 CCTV17 农业农村,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226198/index.m3u8
CGTN,http://live.cgtn.com/1000/prog_index.m3u8 CCTV4K 超高清,
CGTN 法语频道,https://livefr.cgtn.com/1000f/prog_index.m3u8 CCTV8K 超高清,
CGTN 俄语频道,http://liveru.cgtn.com/1000r/prog_index.m3u8
CGTN 阿拉伯语频道,http://livear.cgtn.com/1000a/prog_index.m3u8
CGTN 西班牙语频道,http://livees.cgtn.com/500e/prog_index.m3u8;http://livees.cgtn.com/1000e/prog_index.m3u8
CGTN 纪录频道,https://livedoc.cgtn.com/500d/prog_index.m3u8
风云剧场,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226950/index.m3u8 风云剧场,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226950/index.m3u8
第一剧场,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226959/index.m3u8 第一剧场,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226959/index.m3u8
怀旧剧场,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226972/index.m3u8 怀旧剧场,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226972/index.m3u8
@@ -253,6 +251,13 @@ CGTN 纪录频道,https://livedoc.cgtn.com/500d/prog_index.m3u8
四川卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225768/index.m3u8;http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225768/index.m3u8 四川卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225768/index.m3u8;http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225768/index.m3u8
东南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226341/index.m3u8;http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225766/index.m3u8 东南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226341/index.m3u8;http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225766/index.m3u8
海南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226465/index.m3u8;http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225769/index.m3u8 海南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226465/index.m3u8;http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225769/index.m3u8
国际频道
CGTN,http://live.cgtn.com/1000/prog_index.m3u8
CGTN 法语频道,https://livefr.cgtn.com/1000f/prog_index.m3u8
CGTN 俄语频道,http://liveru.cgtn.com/1000r/prog_index.m3u8
CGTN 阿拉伯语频道,http://livear.cgtn.com/1000a/prog_index.m3u8
CGTN 西班牙语频道,http://livees.cgtn.com/500e/prog_index.m3u8;http://livees.cgtn.com/1000e/prog_index.m3u8
CGTN 纪录频道,https://livedoc.cgtn.com/500d/prog_index.m3u8
""".trimIndent() """.trimIndent()
val map: MutableMap<String, MutableList<TV>> = mutableMapOf() val map: MutableMap<String, MutableList<TV>> = mutableMapOf()
@@ -1,5 +1,7 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.content.res.Resources
import android.util.TypedValue
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -12,4 +14,20 @@ object Utils {
fun getDateTimestamp(): Long { fun getDateTimestamp(): Long {
return Date().time / 1000 return Date().time / 1000
} }
fun dpToPx(dp: Float): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
Resources.getSystem().displayMetrics
).toInt()
}
fun dpToPx(dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
Resources.getSystem().displayMetrics
).toInt()
}
} }
@@ -1,18 +1,27 @@
package com.lizongying.mytv.api package com.lizongying.mytv.api
import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.protobuf.ProtoConverterFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class ApiClient { class ApiClient {
private val yspUrl = "https://player-api.yangshipin.cn/" private val yspUrl = "https://player-api.yangshipin.cn/"
private val myUrl = "https://lyrics.run/" private val myUrl = "https://lyrics.run/"
// private val myUrl = "http://10.0.2.2:8081/" private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/"
private var okHttpClient = getUnsafeOkHttpClient()
val yspApiService: YSPApiService by lazy { val yspApiService: YSPApiService by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(yspUrl) .baseUrl(yspUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build().create(YSPApiService::class.java) .build().create(YSPApiService::class.java)
} }
@@ -20,7 +29,60 @@ class ApiClient {
val yspTokenService: YSPTokenService by lazy { val yspTokenService: YSPTokenService by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl(myUrl) .baseUrl(myUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build().create(YSPTokenService::class.java) .build().create(YSPTokenService::class.java)
} }
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
.client(okHttpClient)
.addConverterFactory(ProtoConverterFactory.create())
.build().create(YSPProtoService::class.java)
}
val yspBtraceService: YSPBtraceService by lazy {
Retrofit.Builder()
.baseUrl(traceUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPBtraceService::class.java)
}
private fun getUnsafeOkHttpClient(): OkHttpClient {
try {
val trustAllCerts: Array<TrustManager> = arrayOf(
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun checkServerTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> {
return emptyArray()
}
}
)
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.dns(DnsCache())
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
} }
@@ -1,17 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class BtraceClient {
private val yspUrl = "https://btrace.yangshipin.cn/"
val yspBtraceService: YSPBtraceService by lazy {
Retrofit.Builder()
.baseUrl(yspUrl)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPBtraceService::class.java)
}
}
@@ -0,0 +1,23 @@
package com.lizongying.mytv.api
import okhttp3.Dns
import java.net.InetAddress
import java.util.concurrent.ConcurrentHashMap
class DnsCache : Dns {
private val dnsCache: MutableMap<String, List<InetAddress>> = ConcurrentHashMap()
override fun lookup(hostname: String): List<InetAddress> {
dnsCache[hostname]?.let {
return it
}
val addresses = InetAddress.getAllByName(hostname).toList()
if (addresses.isNotEmpty()) {
dnsCache[hostname] = addresses
}
return addresses
}
}
@@ -15,6 +15,7 @@ data class LiveInfo(
data class LiveInfoData( data class LiveInfoData(
val chanll: String, val chanll: String,
val playurl: String, val playurl: String,
val errinfo: String,
) )
data class LiveInfoRequest( data class LiveInfoRequest(
@@ -1,16 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Retrofit
import retrofit2.converter.protobuf.ProtoConverterFactory
class ProtoClient {
private val protoUrl = "https://capi.yangshipin.cn/"
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
.addConverterFactory(ProtoConverterFactory.create())
.build().create(YSPProtoService::class.java)
}
}
@@ -70,6 +70,11 @@ class YSP(var context: Context) {
defn = "fhd" defn = "fhd"
randStr = getRand() randStr = getRand()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
timeStr = getTimeStr() timeStr = getTimeStr()
// guid = "lq3oqitm_1e15dnzgjnb" // guid = "lq3oqitm_1e15dnzgjnb"
@@ -105,6 +110,15 @@ class YSP(var context: Context) {
return guid return guid
} }
private fun newGuid(): String {
guid = generateGuid()
with(sharedPref!!.edit()) {
putString("guid", guid)
apply()
}
return guid
}
fun getRand(): String { fun getRand(): String {
var n = "" var n = ""
val e = "ABCDEFGHIJKlMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" val e = "ABCDEFGHIJKlMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
@@ -1,6 +1,5 @@
package com.lizongying.mytv.models package com.lizongying.mytv.models
import android.content.SharedPreferences
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -19,8 +19,14 @@ class TVViewModel(private var tv: TV) : ViewModel() {
private var rowPosition: Int = 0 private var rowPosition: Int = 0
private var itemPosition: Int = 0 private var itemPosition: Int = 0
var retryTimes: Int = 0
var retryMaxTimes: Int = 8
var programUpdateTime: Long = 0 var programUpdateTime: Long = 0
private val _errInfo = MutableLiveData<String>()
val errInfo: LiveData<String>
get() = _errInfo
private val _programId = MutableLiveData<String>() private val _programId = MutableLiveData<String>()
val programId: LiveData<String> val programId: LiveData<String>
get() = _programId get() = _programId
@@ -65,10 +71,31 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val ready: LiveData<Boolean> val ready: LiveData<Boolean>
get() = _ready get() = _ready
private var mMinimumLoadableRetryCount = 3 private var mMinimumLoadableRetryCount = 5
var seq = 0 var seq = 0
var needToken = false
private val channelsNeedToken = arrayOf(
"CCTV3 综艺",
"CCTV6 电影",
"CCTV8 电视剧",
"风云剧场",
"第一剧场",
"怀旧剧场",
"世界地理",
"风云音乐",
"兵器科技",
"风云足球",
"高尔夫网球",
"女性时尚",
"央视文化精品",
"央视台球",
"电视指南",
"卫生健康",
)
fun addVideoUrl(url: String) { fun addVideoUrl(url: String) {
if (_videoUrl.value?.isNotEmpty() == true) { if (_videoUrl.value?.isNotEmpty() == true) {
if (_videoUrl.value!!.last().contains("cctv.cn")) { if (_videoUrl.value!!.last().contains("cctv.cn")) {
@@ -119,6 +146,10 @@ class TVViewModel(private var tv: TV) : ViewModel() {
_pid.value = tv.pid _pid.value = tv.pid
_sid.value = tv.sid _sid.value = tv.sid
_program.value = mutableListOf() _program.value = mutableListOf()
if (tv.title in channelsNeedToken) {
needToken = true
}
} }
fun getRowPosition(): Int { fun getRowPosition(): Int {
@@ -137,6 +168,10 @@ class TVViewModel(private var tv: TV) : ViewModel() {
itemPosition = position itemPosition = position
} }
fun setErrInfo(info: String) {
_errInfo.value = info
}
fun update(t: TV) { fun update(t: TV) {
tv = t tv = t
} }
@@ -187,18 +222,25 @@ class TVViewModel(private var tv: TV) : ViewModel() {
mMinimumLoadableRetryCount = minimumLoadableRetryCount mMinimumLoadableRetryCount = minimumLoadableRetryCount
} }
/**
* (playerView?.player as ExoPlayer).setMediaSource(tvViewModel.buildSource())
*/
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun buildSource(videoUrl: String, mHeaders: Map<String, String>?): HlsMediaSource { fun buildSource(): HlsMediaSource {
val httpDataSource = DefaultHttpDataSource.Factory() val httpDataSource = DefaultHttpDataSource.Factory()
mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) } mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) }
return HlsMediaSource.Factory(httpDataSource).createMediaSource( return HlsMediaSource.Factory(httpDataSource).createMediaSource(
MediaItem.fromUri( MediaItem.fromUri(
Uri.parse(videoUrl) Uri.parse(getVideoUrlCurrent())
) )
) )
} }
fun getVideoUrlCurrent(): String {
return _videoUrl.value!![_videoIndex.value!!]
}
companion object { companion object {
private const val TAG = "TVViewModel" private const val TAG = "TVViewModel"
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/channel_fragment"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="end|top"
android:layout_marginTop="20dp"
android:layout_marginEnd="50dp">
<TextView
android:id="@+id/channel_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="0dp"
android:text="99"
android:textColor="#FFEEEEEE"
android:textStyle="bold"
android:textSize="50sp" />
</FrameLayout>
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="20dp"
android:paddingLeft="7dp"
android:paddingRight="7dp"
android:background="#FF263238"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="16sp" />
<TextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
></TextView>
<Switch
android:id="@+id/switch_channel_reversal"
android:text="@string/title_channel_reversal"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_channel_num"
android:text="@string/title_channel_num"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<ImageView
android:layout_width="300dp"
android:layout_height="wrap_content"
android:src="@drawable/appreciate"
android:background="@color/white" />
</LinearLayout>
</FrameLayout>
-1
View File
@@ -12,7 +12,6 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:contentDescription="@string/logo"
android:padding="10dp" /> android:padding="10dp" />
<LinearLayout <LinearLayout
+2 -1
View File
@@ -1,4 +1,5 @@
<resources> <resources>
<string name="app_name">我的电视</string> <string name="app_name">我的电视</string>
<string name="logo">logo</string> <string name="title_channel_reversal">换台反转</string>
<string name="title_channel_num">数字选台</string>
</resources> </resources>
-1
View File
@@ -1,6 +1,5 @@
<resources> <resources>
<style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"> <style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle">
<item name="lbImageCardViewType">Title|Content</item> <item name="lbImageCardViewType">Title|Content</item>
</style> </style>
+2 -2
View File
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.2.0' apply false id 'com.android.application' version '8.2.1' apply false
id 'com.android.library' version '8.2.0' apply false id 'com.android.library' version '8.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.21' apply false id 'org.jetbrains.kotlin.android' version '1.9.21' apply false
} }