diff --git a/HISTORY.md b/HISTORY.md index 5e2197f..d7806d9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ ## 更新日志 +### v1.1.9 + +* 菜单打开时,不能打开频道列表 +* 频道号大于1000以上时兼容样式 +* 增加收藏功能 + ### v1.1.8 * 频道列表优化 diff --git a/README.md b/README.md index a9bebab..4160ef7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ * 视频源可以设置为本地文件,格式如:file:///mnt/sdcard/tmp/channels.m3u /channels.m3u * 如果设置的是本地文件,则软件重新启动后不会自动更新。需要手动确认。 -* 可能需要在设置页面请求权限 +* 高版本可能需要授权 目前支持的配置格式: @@ -75,8 +75,11 @@ adb install my-tv-0.apk ## TODO -* 收藏夹 * 节目增加预告 +* 兼容4.0 +* 插件商城 +* UI +* 视频解码 ## 赞赏 diff --git a/app/src/main/java/com/lizongying/mytv0/GroupAdapter.kt b/app/src/main/java/com/lizongying/mytv0/GroupAdapter.kt index 0cdde05..ca47e20 100644 --- a/app/src/main/java/com/lizongying/mytv0/GroupAdapter.kt +++ b/app/src/main/java/com/lizongying/mytv0/GroupAdapter.kt @@ -25,7 +25,7 @@ class GroupAdapter( private var listener: ItemListener? = null private var focused: View? = null private var defaultFocused = false - var defaultFocus: Int = -1 + private var defaultFocus: Int = -1 var visiable = false diff --git a/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt b/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt index aee8a7d..f999fab 100644 --- a/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt +++ b/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt @@ -32,7 +32,7 @@ class ListAdapter( private var listener: ItemListener? = null private var focused: View? = null private var defaultFocused = false - var defaultFocus: Int = -1 + private var defaultFocus: Int = -1 var visiable = false @@ -94,6 +94,8 @@ class ListAdapter( view.isFocusableInTouchMode = true // view.alpha = 0.8F + viewHolder.like(tvModel.like.value as Boolean) + if (!defaultFocused && position == defaultFocus) { view.requestFocus() defaultFocused = true @@ -155,6 +157,11 @@ class ListAdapter( }, 0) } + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + tvModel.setLike(!(tvModel.like.value as Boolean)) + viewHolder.like(tvModel.like.value as Boolean) + } + return@setOnKeyListener listener?.onKey(this, keyCode) ?: false } false @@ -221,6 +228,24 @@ class ListAdapter( binding.root.setBackgroundResource(R.color.blur) } } + + fun like(liked: Boolean) { + if (liked) { + binding.heart.setImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_heart + ) + ) + } else { + binding.heart.setImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_heart_empty + ) + ) + } + } } fun toPosition(position: Int) { diff --git a/app/src/main/java/com/lizongying/mytv0/MainActivity.kt b/app/src/main/java/com/lizongying/mytv0/MainActivity.kt index 26d9fb5..a3969ef 100644 --- a/app/src/main/java/com/lizongying/mytv0/MainActivity.kt +++ b/app/src/main/java/com/lizongying/mytv0/MainActivity.kt @@ -31,8 +31,8 @@ class MainActivity : FragmentActivity() { private var settingFragment = SettingFragment() private val handler = Handler(Looper.myLooper()!!) - private val delayHideMenu = 10000L - private val delayHideSetting = 60000L + private val delayHideMenu = 10 * 1000L + private val delayHideSetting = 3 * 60 * 1000L private var doubleBackToExitPressedOnce = false @@ -167,6 +167,18 @@ class MainActivity : FragmentActivity() { } } } + + tvModel.like.observe(this) { _ -> + if (tvModel.like.value != null) { + val liked = tvModel.like.value as Boolean + if (liked) { + TVList.groupModel.getTVListModel(0)?.replaceTVModel(tvModel) + } else { + TVList.groupModel.getTVListModel(0)?.removeTVModel(tvModel.tv.id) + } + SP.setLike(tvModel.tv.id, liked) + } + } } } @@ -594,7 +606,9 @@ class MainActivity : FragmentActivity() { } KeyEvent.KEYCODE_DPAD_LEFT -> { - showFragment(menuFragment) + if (settingFragment.isHidden) { + showFragment(menuFragment) + } } KeyEvent.KEYCODE_DPAD_RIGHT -> { diff --git a/app/src/main/java/com/lizongying/mytv0/MenuFragment.kt b/app/src/main/java/com/lizongying/mytv0/MenuFragment.kt index 0c16c33..f59607a 100644 --- a/app/src/main/java/com/lizongying/mytv0/MenuFragment.kt +++ b/app/src/main/java/com/lizongying/mytv0/MenuFragment.kt @@ -85,12 +85,12 @@ class MenuFragment : Fragment(), GroupAdapter.ItemListener, ListAdapter.ItemList } fun updateList(position: Int) { - val tvListModel = TVList.groupModel.getTVListModel(position) + TVList.groupModel.setPosition(position) + SP.positionGroup = position + val tvListModel = TVList.groupModel.getTVListModel() Log.i(TAG, "updateList tvListModel $position ${tvListModel?.size()}") if (tvListModel != null) { (binding.list.adapter as ListAdapter).update(tvListModel) - TVList.groupModel.setPosition(position) - SP.positionGroup = position } } @@ -165,6 +165,15 @@ class MenuFragment : Fragment(), GroupAdapter.ItemListener, ListAdapter.ItemList groupAdapter.toPosition(TVList.groupModel.position.value!!) return true } +// KeyEvent.KEYCODE_DPAD_RIGHT -> { +// binding.group.visibility = VISIBLE +// groupAdapter.focusable(true) +// listAdapter.focusable(false) +// listAdapter.clear() +// Log.i(TAG, "group toPosition on left") +// groupAdapter.toPosition(TVList.groupModel.position.value!!) +// return true +// } } return false } @@ -181,12 +190,17 @@ class MenuFragment : Fragment(), GroupAdapter.ItemListener, ListAdapter.ItemList // listAdapter.focusable(true) // } + val groupIndex = TVList.getTVModel().groupIndex Log.i( TAG, - "groupIndex ${TVList.getTVModel().groupIndex} ${TVList.groupModel.position.value!!}" + "groupIndex $groupIndex ${TVList.groupModel.position.value!!}" ) - if (TVList.getTVModel().groupIndex == TVList.groupModel.position.value!!) { + if (groupIndex == TVList.groupModel.position.value!!) { + if (listAdapter.tvListModel.getIndex() != TVList.getTVModel().groupIndex) { + updateList(groupIndex) + } + Log.i( TAG, "list on show toPosition ${TVList.getTVModel().tv.title} ${TVList.getTVModel().listIndex}/${listAdapter.tvListModel.size()}" @@ -214,11 +228,6 @@ class MenuFragment : Fragment(), GroupAdapter.ItemListener, ListAdapter.ItemList } } - fun listAdapterToPosition(tvModel: TVModel) { -// listAdapter.toPosition(tvModel.index) -// Log.i(TAG, "listAdapterToPosition ${tvModel.tv.title} ${tvModel.index}/${listAdapter.tvListModel.size()} ${listAdapter.tvListModel.getTVModel(tvModel.index)?.tv?.title}") - } - override fun onResume() { super.onResume() // groupAdapter.toPosition(TVList.groupModel.position.value!!) diff --git a/app/src/main/java/com/lizongying/mytv0/SP.kt b/app/src/main/java/com/lizongying/mytv0/SP.kt index 88bec9a..3ca82f2 100644 --- a/app/src/main/java/com/lizongying/mytv0/SP.kt +++ b/app/src/main/java/com/lizongying/mytv0/SP.kt @@ -31,6 +31,8 @@ object SP { private const val KEY_CHANNEL = "channel" + private const val KEY_LIKE = "like" + private lateinit var sp: SharedPreferences /** @@ -86,4 +88,20 @@ object SP { var channel: Int get() = sp.getInt(KEY_CHANNEL, 0) set(value) = sp.edit().putInt(KEY_CHANNEL, value).apply() + + fun getLike(id: Int): Boolean { + val stringSet = sp.getStringSet(KEY_LIKE, emptySet()) + return stringSet?.contains(id.toString()) ?: false + } + + fun setLike(id: Int, liked: Boolean) { + val stringSet = sp.getStringSet(KEY_LIKE, emptySet())?.toMutableSet() ?: mutableSetOf() + if (liked) { + stringSet.add(id.toString()) + } else { + stringSet.remove(id.toString()) + } + + sp.edit().putStringSet(KEY_LIKE, stringSet).apply() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv0/models/TVGroupModel.kt b/app/src/main/java/com/lizongying/mytv0/models/TVGroupModel.kt index e08a957..7bbc663 100644 --- a/app/src/main/java/com/lizongying/mytv0/models/TVGroupModel.kt +++ b/app/src/main/java/com/lizongying/mytv0/models/TVGroupModel.kt @@ -47,6 +47,10 @@ class TVGroupModel : ViewModel() { getTVListModel(1)?.clear() } + fun getTVListModel(): TVListModel? { + return getTVListModel(position.value as Int) + } + fun getTVListModel(idx: Int): TVListModel? { if (idx >= size()) { return null diff --git a/app/src/main/java/com/lizongying/mytv0/models/TVList.kt b/app/src/main/java/com/lizongying/mytv0/models/TVList.kt index 2b1ad6c..c07a420 100644 --- a/app/src/main/java/com/lizongying/mytv0/models/TVList.kt +++ b/app/src/main/java/com/lizongying/mytv0/models/TVList.kt @@ -34,8 +34,8 @@ object TVList { fun init(context: Context) { _position.value = 0 - groupModel.addTVListModel(TVListModel("我的收藏")) - groupModel.addTVListModel(TVListModel("全部频道")) + groupModel.addTVListModel(TVListModel("我的收藏", 0)) + groupModel.addTVListModel(TVListModel("全部频道", 1)) appDirectory = context.filesDir val file = File(appDirectory, FILE_NAME) @@ -247,7 +247,7 @@ object TVList { var listModelNew: MutableList = mutableListOf() var groupIndex = 2 for ((k, v) in map) { - val tvListModel = TVListModel(k) + val tvListModel = TVListModel(k, groupIndex) for ((listIndex, v1) in v.withIndex()) { v1.groupIndex = groupIndex v1.listIndex = listIndex diff --git a/app/src/main/java/com/lizongying/mytv0/models/TVListModel.kt b/app/src/main/java/com/lizongying/mytv0/models/TVListModel.kt index c0f1f0f..c5b427c 100644 --- a/app/src/main/java/com/lizongying/mytv0/models/TVListModel.kt +++ b/app/src/main/java/com/lizongying/mytv0/models/TVListModel.kt @@ -4,11 +4,15 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -class TVListModel(private val name: String) : ViewModel() { +class TVListModel(private val name: String, private val index: Int) : ViewModel() { fun getName(): String { return name } + fun getIndex(): Int { + return index + } + private val _tvListModel = MutableLiveData>() val tvListModel: LiveData> get() = _tvListModel @@ -44,13 +48,47 @@ class TVListModel(private val name: String) : ViewModel() { _tvListModel.value = newList } + fun removeTVModel(id: Int) { + if (_tvListModel.value == null) { + return + } + val newList = _tvListModel.value!!.toMutableList() + val iterator = newList.iterator() + while (iterator.hasNext()) { + if (iterator.next().tv.id == id) { + iterator.remove() + } + } + _tvListModel.value = newList + } + + fun replaceTVModel(tvModel: TVModel) { + if (_tvListModel.value == null) { + _tvListModel.value = mutableListOf(tvModel) + return + } + + val newList = _tvListModel.value!!.toMutableList() + var exists = false + val iterator = newList.iterator() + while (iterator.hasNext()) { + if (iterator.next().tv.id == tvModel.tv.id) { + exists = true + } + } + if (!exists) { + newList.add(tvModel) + _tvListModel.value = newList + } + } + fun clear() { _tvListModel.value = mutableListOf() setPosition(0) } fun getTVModel(): TVModel? { - return _tvListModel.value?.get(position.value as Int) + return getTVModel(position.value as Int) } fun getTVModel(idx: Int): TVModel? { diff --git a/app/src/main/java/com/lizongying/mytv0/models/TVModel.kt b/app/src/main/java/com/lizongying/mytv0/models/TVModel.kt index f0324db..44c19c0 100644 --- a/app/src/main/java/com/lizongying/mytv0/models/TVModel.kt +++ b/app/src/main/java/com/lizongying/mytv0/models/TVModel.kt @@ -1,5 +1,6 @@ package com.lizongying.mytv0.models +import android.net.Uri import androidx.annotation.OptIn import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -11,6 +12,7 @@ import androidx.media3.exoplayer.dash.DashMediaSource import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.rtsp.RtspMediaSource import androidx.media3.exoplayer.source.MediaSource +import com.lizongying.mytv0.SP //import com.google.android.exoplayer2.source.MediaSource //import com.google.android.exoplayer2.source.MediaSourceFactory @@ -47,18 +49,26 @@ class TVModel(var tv: TV) : ViewModel() { _videoUrl.value = url } - fun getVideoUrl(): String? { + private fun getVideoUrl(): String? { if (_videoIndex.value == null || tv.uris.isEmpty()) { return null } - if (_videoIndex.value!! >= tv.uris.size) { + if (videoIndex.value!! >= tv.uris.size) { return null } return tv.uris[_videoIndex.value!!] } + private val _like = MutableLiveData() + val like: LiveData + get() = _like + + fun setLike(liked: Boolean) { + _like.value = liked + } + private val _ready = MutableLiveData() val ready: LiveData get() = _ready @@ -68,12 +78,13 @@ class TVModel(var tv: TV) : ViewModel() { } private val _videoIndex = MutableLiveData() - val videoIndex: LiveData + private val videoIndex: LiveData get() = _videoIndex init { _position.value = 0 _videoIndex.value = 0 + _like.value = SP.getLike(tv.id) _videoUrl.value = getVideoUrl() _program.value = mutableListOf() } @@ -85,6 +96,10 @@ class TVModel(var tv: TV) : ViewModel() { @OptIn(UnstableApi::class) fun buildSource(): MediaSource? { val url = getVideoUrl() ?: return null + val uri = Uri.parse(url) ?: return null + val path = uri.path ?: return null + val scheme = uri.scheme ?: return null + var userAgent = "" val httpDataSource = DefaultHttpDataSource.Factory() tv.headers?.let { @@ -96,12 +111,13 @@ class TVModel(var tv: TV) : ViewModel() { } } } - val mediaItem = MediaItem.fromUri(getVideoUrl()!!) - return if (url.lowercase().endsWith(".m3u8")) { + + val mediaItem = MediaItem.fromUri(uri.toString()) + return if (path.lowercase().endsWith(".m3u8")) { HlsMediaSource.Factory(httpDataSource).createMediaSource(mediaItem) - } else if (url.lowercase().endsWith(".mpd")) { + } else if (path.lowercase().endsWith(".mpd")) { DashMediaSource.Factory(httpDataSource).createMediaSource(mediaItem) - } else if (url.lowercase().endsWith("rtsp:")) { + } else if (scheme.lowercase() == "rtsp") { if (userAgent.isEmpty()) { RtspMediaSource.Factory().createMediaSource(mediaItem) } else { diff --git a/app/src/main/res/layout/channel.xml b/app/src/main/res/layout/channel.xml index fde1a60..80f0700 100644 --- a/app/src/main/res/layout/channel.xml +++ b/app/src/main/res/layout/channel.xml @@ -13,7 +13,7 @@ @@ -31,12 +33,14 @@ android:id="@+id/heart" android:layout_width="30dp" android:layout_height="30dp" - android:src="@drawable/ic_heart_empty_light" + android:src="@drawable/ic_heart_empty" android:contentDescription="Heart Icon" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" + android:clickable="true" + android:focusable="false" android:padding="5dp" - android:layout_marginEnd="5dp" /> + android:layout_marginEnd="5dp"/> - - -