diff --git a/HISTORY.md b/HISTORY.md index 86766acf..bfdb6934 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,16 @@ ## 更新日誌 +### v1.3.8.3 + +* 修復視頻源導入成功時卻顯示錯誤的問題 +* 擴寬視頻源選擇界面 +* 視頻源按添加時間倒序排列 +* 加速GitHub視頻源導入 + +### v1.3.8.2-kitkat + +* 支持多源選擇 + ### v1.3.8.2 * 支持多源選擇 @@ -8,6 +19,10 @@ * 支持rtmp +### v1.3.8.0-kitkat + +* 優化遠程配置 + ### v1.3.8.0 * 優化遠程配置 diff --git a/README.md b/README.md index 4ce9c333..1243f83c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ 注意: +* 遇到問題可以先考慮重啟/恢復默認/清除數據/重新安裝等方式自助解決 * 視頻源可以設置為本地文件,格式如:file:///mnt/sdcard/tmp/channels.m3u /channels.m3u diff --git a/app/src/main/java/com/lizongying/mytv0/MainActivity.kt b/app/src/main/java/com/lizongying/mytv0/MainActivity.kt index 7f3bd5c7..9976738d 100644 --- a/app/src/main/java/com/lizongying/mytv0/MainActivity.kt +++ b/app/src/main/java/com/lizongying/mytv0/MainActivity.kt @@ -221,7 +221,7 @@ class MainActivity : AppCompatActivity() { .use { i -> val channels = i.readText() if (channels.isNotEmpty()) { - viewModel.tryStr2List(channels, null, "") + viewModel.tryStr2Channels(channels, null, "") } else { Log.w(TAG, "$it is empty") } diff --git a/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt b/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt index ae12338c..c439104d 100644 --- a/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt +++ b/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt @@ -21,7 +21,6 @@ import com.lizongying.mytv0.models.TVModel import com.lizongying.mytv0.requests.HttpClient import com.lizongying.mytv0.showToast import io.github.lizongying.Gua -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -35,7 +34,7 @@ class MainViewModel : ViewModel() { var listModel: List = listOf() val groupModel = TVGroupModel() private var cacheFile: File? = null - private var cacheConfig = "" + private var cacheChannels = "" private var initialized = false val sources = Sources() @@ -67,7 +66,7 @@ class MainViewModel : ViewModel() { if (it.startsWith("http")) { viewModelScope.launch { Log.i(TAG, "updateConfig $it") - update(it) + importFromUrl(it) SP.epg?.let { i -> updateEPG(i) } @@ -95,17 +94,17 @@ class MainViewModel : ViewModel() { cacheFile!!.createNewFile() } - cacheConfig = getCache() + cacheChannels = getCache() - if (cacheConfig.isEmpty()) { - cacheConfig = context.resources.openRawResource(R.raw.channels).bufferedReader() + if (cacheChannels.isEmpty()) { + cacheChannels = context.resources.openRawResource(R.raw.channels).bufferedReader() .use { it.readText() } } - Log.i(TAG, "cacheConfig $cacheConfig") + Log.i(TAG, "cacheChannels $cacheChannels") try { - str2List(cacheConfig) + str2Channels(cacheChannels) } catch (e: Exception) { e.printStackTrace() cacheFile!!.deleteOnExit() @@ -142,35 +141,68 @@ class MainViewModel : ViewModel() { } } - suspend fun update(serverUrl: String) { + private suspend fun importFromUrl(serverUrl: String) { Log.i(TAG, "request $serverUrl") - try { - withContext(Dispatchers.IO) { - val request = okhttp3.Request.Builder().url(serverUrl).build() - val response = HttpClient.okHttpClient.newCall(request).execute() + val urls = + if (serverUrl.startsWith("https://raw.githubusercontent.com") || serverUrl.startsWith("https://github.com")) { + listOf( + "https://ghp.ci/", + "https://gh.llkk.cc/", + "https://github.moeyy.xyz/", + "https://mirror.ghproxy.com/", + "https://ghproxy.cn/", + "https://ghproxy.net/", + "https://ghproxy.click/", + "https://ghproxy.com/", + "https://github.moeyy.cn/", + "https://gh-proxy.llyke.com/", + ).map { + Pair("$it$serverUrl", serverUrl) + } + } else { + listOf(Pair(serverUrl, serverUrl)) + } - if (response.isSuccessful) { - val str = response.body?.string() ?: "" - withContext(Dispatchers.Main) { - tryStr2List(str, null, serverUrl) + var err = 0 + var shouldBreak = false + for ((a, b) in urls) { + try { + withContext(Dispatchers.IO) { + val request = okhttp3.Request.Builder().url(a).build() + val response = HttpClient.okHttpClient.newCall(request).execute() + + if (response.isSuccessful) { + val str = response.body?.string() ?: "" + withContext(Dispatchers.Main) { + tryStr2Channels(str, null, b) + } + err = 0 + shouldBreak = true + } else { + Log.e(TAG, "Request status ${response.code}") + err = R.string.channel_status_error } - } else { - Log.e(TAG, "Request status ${response.code}") - R.string.channel_status_error.showToast() } + } catch (e: JsonSyntaxException) { + e.printStackTrace() + Log.e(TAG, "JSON Parse Error", e) + err = R.string.channel_format_error + shouldBreak = true + } catch (e: NullPointerException) { + e.printStackTrace() + Log.e(TAG, "Null Pointer Error", e) + err = R.string.channel_read_error + } catch (e: Exception) { + e.printStackTrace() + Log.e(TAG, "Request error $e") + err = R.string.channel_request_error } - } catch (e: JsonSyntaxException) { - e.printStackTrace() - Log.e("JSON Parse Error", e.toString()) - R.string.channel_format_error.showToast() - } catch (e: NullPointerException) { - e.printStackTrace() - Log.e("Null Pointer Error", e.toString()) - R.string.channel_read_error.showToast() - } catch (e: Exception) { - e.printStackTrace() - Log.e(TAG, "Request error $e") - R.string.channel_request_error.showToast() + + if (shouldBreak) break + } + + if (err != 0) { + err.showToast() } } @@ -179,14 +211,14 @@ class MainViewModel : ViewModel() { .use { it.readText() } try { - str2List(str) + str2Channels(str) } catch (e: Exception) { e.printStackTrace() R.string.channel_read_error.showToast() } } - fun parseUri(uri: Uri) { + fun importFromUri(uri: Uri) { if (uri.scheme == "file") { val file = uri.toFile() Log.i(TAG, "file $file") @@ -197,19 +229,19 @@ class MainViewModel : ViewModel() { return } - tryStr2List(str, file, uri.toString()) + tryStr2Channels(str, file, uri.toString()) } else { - CoroutineScope(Dispatchers.IO).launch { - update(uri.toString()) + viewModelScope.launch { + importFromUrl(uri.toString()) } } } - fun tryStr2List(str: String, file: File?, url: String) { + fun tryStr2Channels(str: String, file: File?, url: String) { try { - if (str2List(str)) { + if (str2Channels(str)) { cacheFile!!.writeText(str) - cacheConfig = str + cacheChannels = str if (url.isNotEmpty()) { SP.config = url sources.addSource( @@ -230,10 +262,10 @@ class MainViewModel : ViewModel() { } } - private fun str2List(str: String): Boolean { + private fun str2Channels(str: String): Boolean { var string = str - if (initialized && string == cacheConfig) { - Log.w(TAG, "same config") + if (initialized && string == cacheChannels) { + Log.w(TAG, "same channels") return false } @@ -241,13 +273,14 @@ class MainViewModel : ViewModel() { if (g.verify(str)) { string = g.decode(str) } + if (string.isEmpty()) { - Log.w(TAG, "config is empty") + Log.w(TAG, "channels is empty") return false } - if (initialized && string == cacheConfig) { - Log.w(TAG, "same config") + if (initialized && string == cacheChannels) { + Log.w(TAG, "same channels") return false } diff --git a/app/src/main/java/com/lizongying/mytv0/SP.kt b/app/src/main/java/com/lizongying/mytv0/SP.kt index f4540d65..ba3e6101 100644 --- a/app/src/main/java/com/lizongying/mytv0/SP.kt +++ b/app/src/main/java/com/lizongying/mytv0/SP.kt @@ -4,6 +4,9 @@ package com.lizongying.mytv0 import android.content.Context import android.content.SharedPreferences import android.util.Log +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.lizongying.mytv0.data.Source object SP { private const val TAG = "SP" @@ -54,18 +57,68 @@ object SP { private const val KEY_SOURCES = "sources" - private const val KEY_SOURCE_ID = "source_id" - const val DEFAULT_CONFIG_URL = "" const val DEFAULT_CHANNEL_NUM = false const val DEFAULT_EPG = "https://live.fanmingming.com/e.xml" const val DEFAULT_CHANNEL = 0 - const val DEFAULT_SHOW_ALL_CHANNELS = false + const val DEFAULT_SHOW_ALL_CHANNELS = true const val DEFAULT_COMPACT_MENU = true const val DEFAULT_DISPLAY_SECONDS = false const val DEFAULT_LOG_TIMES = 10 const val DEFAULT_POSITION_GROUP = 1 const val DEFAULT_POSITION = 0 + val DEFAULT_SOURCES = Gson().toJson(listOf( + "https://live.fanmingming.com/tv/m3u/ipv6.m3u", + "https://live.fanmingming.com/tv/m3u/itv.m3u", + "https://live.fanmingming.com/tv/m3u/index.m3u", + + "https://iptv-org.github.io/iptv/index.m3u", + + // https://github.com/Guovin/iptv-api + "https://raw.githubusercontent.com/Guovin/iptv-api/gd/output/result.m3u", + + // https://github.com/joevess/IPTV + "https://raw.githubusercontent.com/joevess/IPTV/main/sources/iptv_sources.m3u", + "https://raw.githubusercontent.com/joevess/IPTV/main/sources/home_sources.m3u", + "https://raw.githubusercontent.com/joevess/IPTV/main/iptv.m3u8", + "https://raw.githubusercontent.com/joevess/IPTV/main/home.m3u8", + + // https://github.com/zbefine/iptv + "https://raw.githubusercontent.com/zbefine/iptv/main/iptv.m3u", + + // https://github.com/YanG-1989/m3u + "https://raw.githubusercontent.com/YanG-1989/m3u/main/Gather.m3u", + + // https://github.com/YueChan/Live + "https://raw.githubusercontent.com/YueChan/Live/main/APTV.m3u", + "https://raw.githubusercontent.com/YueChan/Live/main/Global.m3u", + "https://raw.githubusercontent.com/YueChan/Live/main/IPTV.m3u", + + "https://freetv.fun/test_channels_new.m3u", + + // https://github.com/SPX372928/MyIPTV + "https://raw.githubusercontent.com/SPX372928/MyIPTV/master/%E9%BB%91%E9%BE%99%E6%B1%9FPLTV%E7%A7%BB%E5%8A%A8CDN%E7%89%88.txt", + + // https://github.com/vbskycn/iptv + "https://live.zbds.top/tv/iptv6.m3u", + "https://ghp.ci/raw.githubusercontent.com/vbskycn/iptv/refs/heads/master/tv/iptv4.m3u", + + // https://github.com/yuanzl77/IPTV + "http://175.178.251.183:6689/live.m3u", + + // https://github.com/BurningC4/Chinese-IPTV + "https://raw.githubusercontent.com/BurningC4/Chinese-IPTV/master/TV-IPV4.m3u", + + // https://github.com/Moexin/IPTV + "https://raw.githubusercontent.com/Moexin/IPTV/Files/CCTV.m3u", + "https://raw.githubusercontent.com/Moexin/IPTV/Files/CNTV.m3u", + "https://raw.githubusercontent.com/Moexin/IPTV/Files/IPTV.m3u", + ).map { + Source( + uri = it + ) + }, object : TypeToken>() {}.type + ) ?: "" private lateinit var sp: SharedPreferences @@ -181,10 +234,6 @@ object SP { set(value) = sp.edit().putInt(KEY_LOG_TIMES, value).apply() var sources: String? - get() = sp.getString(KEY_SOURCES, "") + get() = sp.getString(KEY_SOURCES, DEFAULT_SOURCES) set(value) = sp.edit().putString(KEY_SOURCES, value).apply() - - var sourceId: String? - get() = sp.getString(KEY_SOURCE_ID, "") - set(value) = sp.edit().putString(KEY_SOURCE_ID, value).apply() } \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv0/SettingFragment.kt b/app/src/main/java/com/lizongying/mytv0/SettingFragment.kt index 91189552..a673428a 100644 --- a/app/src/main/java/com/lizongying/mytv0/SettingFragment.kt +++ b/app/src/main/java/com/lizongying/mytv0/SettingFragment.kt @@ -295,6 +295,8 @@ class SettingFragment : Fragment() { viewModel.groupModel.setPosition(SP.DEFAULT_POSITION_GROUP) viewModel.groupModel.setPositionPlaying(SP.DEFAULT_POSITION_GROUP) + SP.sources = SP.DEFAULT_SOURCES + SP.config = SP.DEFAULT_CONFIG_URL Log.i(TAG, "config url: ${SP.config}") context.deleteFile(CACHE_FILE_NAME) @@ -351,7 +353,7 @@ class SettingFragment : Fragment() { if (uri.scheme == "file") { requestReadPermissions() } else { - viewModel.parseUri(uri) + viewModel.importFromUri(uri) } } else { R.string.invalid_config_address.showToast() @@ -437,7 +439,7 @@ class SettingFragment : Fragment() { PERMISSIONS_REQUEST_CODE ) } else { - viewModel.parseUri(uri) + viewModel.importFromUri(uri) } } @@ -449,7 +451,7 @@ class SettingFragment : Fragment() { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == PERMISSION_READ_EXTERNAL_STORAGE_REQUEST_CODE) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - viewModel.parseUri(uri) + viewModel.importFromUri(uri) } else { R.string.authorization_failed.showToast() } diff --git a/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt b/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt index e25c0eec..b80624db 100644 --- a/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt +++ b/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt @@ -106,7 +106,7 @@ class SimpleServer(private val context: Context, private val viewModel: MainView try { readBody(session)?.let { handler.post { - viewModel.tryStr2List(it, null, "") + viewModel.tryStr2Channels(it, null, "") } } } catch (e: Exception) { @@ -129,7 +129,7 @@ class SimpleServer(private val context: Context, private val viewModel: MainView if (req.uri != null) { val uri = Uri.parse(req.uri) handler.post { - viewModel.parseUri(uri) + viewModel.importFromUri(uri) } } } diff --git a/app/src/main/java/com/lizongying/mytv0/SourcesAdapter.kt b/app/src/main/java/com/lizongying/mytv0/SourcesAdapter.kt index 1a0a3275..c987cb19 100644 --- a/app/src/main/java/com/lizongying/mytv0/SourcesAdapter.kt +++ b/app/src/main/java/com/lizongying/mytv0/SourcesAdapter.kt @@ -9,9 +9,9 @@ import androidx.core.content.ContextCompat import androidx.core.view.marginStart import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.lizongying.mytv0.data.Source import com.lizongying.mytv0.databinding.SourcesItemBinding import com.lizongying.mytv0.models.Sources +import java.util.Locale class SourcesAdapter( @@ -21,11 +21,6 @@ class SourcesAdapter( ) : RecyclerView.Adapter() { private var listener: ItemListener? = null - private var focused: View? = null - private var defaultFocused = false - private var defaultFocus: Int = -1 - - var visiable = false val application = context.applicationContext as MyTVApplication @@ -49,60 +44,26 @@ class SourcesAdapter( return ViewHolder(context, binding) } - fun update(sources: Sources) { - this.sources = sources - recyclerView.post { - notifyDataSetChanged() - } - } - - fun clear() { - focused?.clearFocus() - recyclerView.invalidate() - } - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { sources.let { val source = it.getSource(position)!! val view = viewHolder.itemView - view.isFocusable = true view.isFocusableInTouchMode = true - viewHolder.check(position == it.checkedValue) - - viewHolder.binding.heart.setOnClickListener { _ -> - it.setChecked(position) - viewHolder.check(true) - } + viewHolder.checked(source.checked) - if (!defaultFocused && position == defaultFocus) { - view.requestFocus() - defaultFocused = true - } + view.setOnFocusChangeListener { _, hasFocus -> + listener?.onItemFocusChange(position, hasFocus) - val onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> - listener?.onItemFocusChange(source, hasFocus) - - if (hasFocus) { - viewHolder.focus(true) - focused = view - if (visiable) { - if (position != it.focusedValue) { - it.setFocused(position) - } - } else { - visiable = true - } - } else { - viewHolder.focus(false) - } + viewHolder.focus(hasFocus) } - view.onFocusChangeListener = onFocusChangeListener - view.setOnClickListener { _ -> + it.setChecked(position) + // ui + check(position) listener?.onItemClicked(position) } @@ -138,20 +99,12 @@ class SourcesAdapter( }, 0) } - if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER - ) { - it.setChecked(position) - check(position) - listener?.onItemClicked(position) - } - - return@setOnKeyListener listener?.onKey(this, keyCode) ?: false + return@setOnKeyListener listener?.onKey(keyCode) ?: false } false } + viewHolder.bindNum(String.format(Locale.getDefault(), "%02d", position)) viewHolder.bindTitle(source.uri) } } @@ -160,6 +113,10 @@ class SourcesAdapter( class ViewHolder(private val context: Context, val binding: SourcesItemBinding) : RecyclerView.ViewHolder(binding.root) { + fun bindNum(text: String) { + binding.num.text = text + } + fun bindTitle(text: String) { binding.title.text = text } @@ -174,8 +131,9 @@ class SourcesAdapter( } } - fun check(checked: Boolean) { - binding.heart.visibility = if (checked) View.VISIBLE else View.GONE + // show done icon + fun checked(isChecked: Boolean) { + binding.heart.visibility = if (isChecked) View.VISIBLE else View.GONE } } @@ -202,6 +160,12 @@ class SourcesAdapter( } } + fun changed() { + recyclerView.post { + notifyDataSetChanged() + } + } + fun toPosition(position: Int) { recyclerView.post { (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( @@ -217,9 +181,9 @@ class SourcesAdapter( } interface ItemListener { - fun onItemFocusChange(source: Source, hasFocus: Boolean) - fun onItemClicked(position: Int, tag: String = "sources") - fun onKey(listAdapter: SourcesAdapter, keyCode: Int): Boolean + fun onItemFocusChange(position: Int, hasFocus: Boolean, tag: String = TAG) + fun onItemClicked(position: Int, tag: String = TAG) + fun onKey(keyCode: Int, tag: String = TAG): Boolean } fun setItemListener(listener: ItemListener) { @@ -227,7 +191,7 @@ class SourcesAdapter( } companion object { - private const val TAG = "ListAdapter" + private const val TAG = "SourcesAdapter" } } diff --git a/app/src/main/java/com/lizongying/mytv0/SourcesFragment.kt b/app/src/main/java/com/lizongying/mytv0/SourcesFragment.kt index 311fec77..d2047311 100644 --- a/app/src/main/java/com/lizongying/mytv0/SourcesFragment.kt +++ b/app/src/main/java/com/lizongying/mytv0/SourcesFragment.kt @@ -5,7 +5,6 @@ import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,7 +12,6 @@ import android.view.WindowManager import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager -import com.lizongying.mytv0.data.Source import com.lizongying.mytv0.databinding.SourcesBinding @@ -65,14 +63,16 @@ class SourcesFragment : DialogFragment(), SourcesAdapter.ItemListener { handler.postDelayed(hideFragment, delayHideFragment) - viewModel.sources.removed.observe(this) { p -> - Log.i(TAG, "sources changed") - sourcesAdapter.removed(p.first) + viewModel.sources.removed.observe(this) { items -> + sourcesAdapter.removed(items.first) } - viewModel.sources.added.observe(this) { p -> - Log.i(TAG, "sources changed") - sourcesAdapter.added(p.first) + viewModel.sources.added.observe(this) { items -> + sourcesAdapter.added(items.first) + } + + viewModel.sources.changed.observe(this) { _ -> + sourcesAdapter.changed() } } @@ -88,26 +88,25 @@ class SourcesFragment : DialogFragment(), SourcesAdapter.ItemListener { handler.removeCallbacksAndMessages(null) } - companion object { - const val TAG = "SourcesFragment" - } - - override fun onItemFocusChange(source: Source, hasFocus: Boolean) { -// TODO("Not yet implemented") + override fun onItemFocusChange(position: Int, hasFocus: Boolean, tag: String) { } override fun onItemClicked(position: Int, tag: String) { viewModel.sources.getSource(position)?.let { val uri = Uri.parse(it.uri) handler.post { - viewModel.parseUri(uri) + viewModel.importFromUri(uri) } } } - override fun onKey(listAdapter: SourcesAdapter, keyCode: Int): Boolean { + override fun onKey(keyCode: Int, tag: String): Boolean { handler.removeCallbacks(hideFragment) handler.postDelayed(hideFragment, delayHideFragment) return false } + + companion object { + const val TAG = "SourcesFragment" + } } \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv0/models/Sources.kt b/app/src/main/java/com/lizongying/mytv0/models/Sources.kt index 30f3e10a..fbb708cd 100644 --- a/app/src/main/java/com/lizongying/mytv0/models/Sources.kt +++ b/app/src/main/java/com/lizongying/mytv0/models/Sources.kt @@ -1,6 +1,5 @@ package com.lizongying.mytv0.models -import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.gson.Gson @@ -11,6 +10,7 @@ import com.lizongying.mytv0.data.Source class Sources { private val type = object : TypeToken>() {}.type var version = 0 + private val _removed = MutableLiveData>() val removed: LiveData> get() = _removed @@ -19,22 +19,16 @@ class Sources { val added: LiveData> get() = _added + private val _changed = MutableLiveData() + val changed: LiveData + get() = _changed + private val _sources = MutableLiveData>() val sources: LiveData> get() = _sources private val sourcesValue: List get() = _sources.value ?: listOf() - private val _focused = MutableLiveData() - val focused: LiveData - get() = _focused - val focusedValue: Int - get() = _focused.value ?: DEFAULT_FOCUSED - - fun setFocused(position: Int) { - _focused.value = position - } - private val _checked = MutableLiveData() val checked: LiveData get() = _checked @@ -46,10 +40,6 @@ class Sources { SP.config = getSource(position)!!.uri } - fun setChecked() { - setChecked(focusedValue) - } - fun setSourceChecked(position: Int, checked: Boolean): Boolean { val checkedBefore = getSource(position)?.checked if (checkedBefore == checked) { @@ -64,14 +54,6 @@ class Sources { } } - private val _change = MutableLiveData() - val change: LiveData - get() = _change - - fun setChange() { - _change.value = true - } - private fun setSources(sources: List) { _sources.value = sources SP.sources = Gson().toJson(sources, type) ?: "" @@ -80,185 +62,90 @@ class Sources { fun addSource(source: Source) { if (_sources.value == null) { _sources.value = mutableListOf(source) - return } val index = sourcesValue.indexOfFirst { it.uri == source.uri } if (index == -1) { - val newList = sourcesValue.toMutableList().apply { - add(source) + _sources.value = sourcesValue.toMutableList().apply { + add(0, source) } - _sources.value = newList - SP.sources = Gson().toJson(sources.value, type) ?: "" + SP.sources = Gson().toJson(sourcesValue, type) ?: "" - _added.value = Pair(newList.size, version) + _added.value = Pair(sourcesValue.size - 1, version) version++ } } fun removeSource(id: String) { - if (_sources.value == null) { + if (sourcesValue.isEmpty()) { return } val index = sourcesValue.indexOfFirst { it.id == id } if (index != -1) { - val newList = sourcesValue.toMutableList().apply { + _sources.value = sourcesValue.toMutableList().apply { removeAt(index) } - _sources.value = newList - SP.sources = Gson().toJson(sources.value, type) ?: "" + SP.sources = Gson().toJson(sourcesValue, type) ?: "" _removed.value = Pair(index, version) version++ } } - fun replaceSource(source: Source) { - if (_sources.value == null) { - _sources.value = mutableListOf(source) - return - } - - val newList = sourcesValue.toMutableList() - var exists = false - val iterator = newList.iterator() - while (iterator.hasNext()) { - if (iterator.next().id == source.id) { - exists = true - } - } - if (!exists) { - newList.add(source) - _sources.value = newList - } - } - - fun getSource(): Source? { - return getSource(focusedValue) - } - fun getSource(idx: Int): Source? { if (idx >= size()) { return null } - setFocused(idx) - return sourcesValue[idx] - } - - fun getCurrent(): Source? { - if (focusedValue < 0 || focusedValue >= size()) { - return getSource(0) - } - - return getSource(focusedValue) - } - - fun getPrev(): Source? { - if (size() == 0) { - return null - } - - val p = (size() + focusedValue - 1) % size() - setFocused(p) - return sourcesValue[p] - } - - fun getNext(): Source? { - if (size() == 0) { + if (sourcesValue.isEmpty()) { return null } - val p = (focusedValue + 1) % size() - setFocused(p) - return sourcesValue[p] - } - - fun clearData() { - setSources(listOf()) - setFocused(DEFAULT_FOCUSED) - setChecked(DEFAULT_CHECKED) + return sourcesValue[idx] } - init { - SP.sources?.let { - if (it.isEmpty()) { - Log.i(TAG, "sources is empty") - return@let - } - + fun init() { + if (!SP.sources.isNullOrEmpty()) { try { - val sources: List = Gson().fromJson(it, type) + val sources: List = Gson().fromJson(SP.sources!!, type) setSources(sources) } catch (e: Exception) { e.printStackTrace() - SP.sources = "" + SP.sources = SP.DEFAULT_SOURCES } } - if (size() == 0) { - if (!SP.config.isNullOrEmpty()) { - addSource( - Source( - uri = SP.config!!, - ) + if (!SP.config.isNullOrEmpty()) { + addSource( + Source( + uri = SP.config!!, ) - } - } - - if (size() > -1) { - listOf( - "https://live.fanmingming.com/tv/m3u/ipv6.m3u", - "https://live.fanmingming.com/tv/m3u/itv.m3u", - "https://live.fanmingming.com/tv/m3u/index.m3u", - - "https://iptv-org.github.io/iptv/index.m3u", - - // https://github.com/Guovin/iptv-api - "https://ghp.ci/raw.githubusercontent.com/Guovin/iptv-api/gd/output/result.m3u", - "https://ghp.ci/raw.githubusercontent.com/Guovin/iptv-api/gd/output/result.txt", - - // https://github.com/joevess/IPTV - "https://mirror.ghproxy.com/raw.githubusercontent.com/joevess/IPTV/main/sources/iptv_sources.m3u", - "https://mirror.ghproxy.com/raw.githubusercontent.com/joevess/IPTV/main/sources/home_sources.m3u", - "https://mirror.ghproxy.com/raw.githubusercontent.com/joevess/IPTV/main/iptv.m3u", - "https://mirror.ghproxy.com/raw.githubusercontent.com/joevess/IPTV/main/home.m3u", - - // https://github.com/zbefine/iptv - "https://cdn.jsdelivr.net/gh/zbefine/iptv/iptv.m3u", - "https://cdn.jsdelivr.net/gh/zbefine/iptv/iptv.txt", - ).forEach { - addSource( - Source( - uri = it - ), - ) - } + ) } if (size() > 0) { _checked.value = sourcesValue.indexOfFirst { it.uri == SP.config } - if (_checked.value != null && _checked.value!! > -1) { - setSourceChecked(_checked.value!!, true) + if (checkedValue > -1) { + setSourceChecked(checkedValue, true) } - - _focused.value = _checked.value } + + _changed.value = version + version++ } - fun size(): Int { - if (_sources.value == null) { - return 0 - } + init { + init() + } + fun size(): Int { return sourcesValue.size } companion object { const val TAG = "Sources" - const val DEFAULT_FOCUSED = 0 const val DEFAULT_CHECKED = -1 } } \ 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 318a0a90..38899e38 100644 --- a/app/src/main/java/com/lizongying/mytv0/models/TVGroupModel.kt +++ b/app/src/main/java/com/lizongying/mytv0/models/TVGroupModel.kt @@ -168,7 +168,8 @@ class TVGroupModel : ViewModel() { return null } - var tvListModel = getCurrentList()!! + var tvListModel = getCurrentList() ?: return null + if (keep) { Log.i(TAG, "group position $positionValue") return tvListModel.getPrev() diff --git a/app/src/main/res/layout/sources.xml b/app/src/main/res/layout/sources.xml index e8da9e41..ff287fa9 100644 --- a/app/src/main/res/layout/sources.xml +++ b/app/src/main/res/layout/sources.xml @@ -9,8 +9,8 @@ diff --git a/app/src/main/res/layout/sources_item.xml b/app/src/main/res/layout/sources_item.xml index 776a7d92..2c68c2ff 100644 --- a/app/src/main/res/layout/sources_item.xml +++ b/app/src/main/res/layout/sources_item.xml @@ -8,16 +8,30 @@ android:layout_marginBottom="1dp" android:background="@color/blur"> + + input[type="text"] { + color: #EEEEEE; + } @@ -89,12 +93,13 @@

我的電視·〇

視頻源可以设置為地址/文本/文件其中之一


-
+
+

@@ -218,7 +223,12 @@

視頻源可以设置為地址/文本/文件其中之一

let htmlString = `
`; let doc = new DOMParser().parseFromString(htmlString, 'text/html'); - source.appendChild(doc.body.firstChild); + const firstChild = source.firstChild; + if (firstChild) { + source.insertBefore(doc.body.firstChild, firstChild); + } else { + source.appendChild(doc.body.firstChild); + } } } diff --git a/version.json b/version.json index 0a197a86..3b3eaf5a 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version_code": 16975874, "version_name": "v1.3.8.2"} +{"version_code": 16975875, "version_name": "v1.3.8.3"}