一些修改提交
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
# CodeGraph data files
|
||||||
|
# These are local to each machine and should not be committed
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
cache/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Hook markers
|
||||||
|
.dirty
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"pid": 38368,
|
||||||
|
"version": "0.9.7",
|
||||||
|
"socketPath": "\\\\.\\pipe\\codegraph-cb339f1c8a0989e3",
|
||||||
|
"startedAt": 1781679324951
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260617-065519_fbde6a25cff2683a": 1781679319697
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260617-065519_fbde6a25cff2683a": "auto"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260617-065519_fbde6a25cff2683a": "我要给电视盒子做一个非常简单的APP…"
|
||||||
|
}
|
||||||
@@ -27,11 +27,14 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +45,9 @@ dependencies {
|
|||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.media3.exoplayer)
|
||||||
|
implementation(libs.androidx.media3.exoplayer.hls)
|
||||||
|
implementation(libs.androidx.media3.ui)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -11,16 +17,32 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.FengTv"
|
android:theme="@style/Theme.FengTv"
|
||||||
|
android:banner="@mipmap/ic_launcher"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:excludeFromRecents="false"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="landscape"
|
||||||
|
android:configChanges="orientation|keyboardHidden|screenSize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package cn.flyzxf.FengTv
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
||||||
|
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(launchIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package cn.flyzxf.FengTv
|
||||||
|
|
||||||
|
data class ChannelInfo(
|
||||||
|
val name: String,
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package cn.flyzxf.FengTv
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileReader
|
||||||
|
import java.io.FileWriter
|
||||||
|
|
||||||
|
object ChannelManager {
|
||||||
|
private const val PREFS_NAME = "fengtv_prefs"
|
||||||
|
private const val KEY_SOURCE_URL = "source_url"
|
||||||
|
private const val KEY_CURRENT_INDEX = "current_index"
|
||||||
|
private const val CHANNEL_FILE = "channels.txt"
|
||||||
|
|
||||||
|
private var channels: MutableList<ChannelInfo> = mutableListOf()
|
||||||
|
var currentIndex: Int = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun loadChannels(context: Context): List<ChannelInfo> {
|
||||||
|
val file = File(context.filesDir, CHANNEL_FILE)
|
||||||
|
channels.clear()
|
||||||
|
if (file.exists()) {
|
||||||
|
try {
|
||||||
|
BufferedReader(FileReader(file)).use { reader ->
|
||||||
|
reader.forEachLine { line ->
|
||||||
|
val trimmed = line.trim()
|
||||||
|
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) {
|
||||||
|
val commaIdx = trimmed.indexOf(',')
|
||||||
|
if (commaIdx > 0 && commaIdx < trimmed.length - 1) {
|
||||||
|
val name = trimmed.substring(0, commaIdx).trim()
|
||||||
|
val url = trimmed.substring(commaIdx + 1).trim()
|
||||||
|
if (url.isNotEmpty()) {
|
||||||
|
channels.add(ChannelInfo(name, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val prefs = getPrefs(context)
|
||||||
|
currentIndex = prefs.getInt(KEY_CURRENT_INDEX, 0)
|
||||||
|
if (currentIndex >= channels.size) currentIndex = 0
|
||||||
|
return channels.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveChannels(context: Context, lines: List<String>) {
|
||||||
|
val file = File(context.filesDir, CHANNEL_FILE)
|
||||||
|
try {
|
||||||
|
FileWriter(file).use { writer ->
|
||||||
|
lines.forEach { line ->
|
||||||
|
writer.write(line + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
loadChannels(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSavedSourceUrl(context: Context): String? {
|
||||||
|
return getPrefs(context).getString(KEY_SOURCE_URL, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveSourceUrl(context: Context, url: String) {
|
||||||
|
getPrefs(context).edit().putString(KEY_SOURCE_URL, url).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCurrentIndex(context: Context, index: Int) {
|
||||||
|
currentIndex = index
|
||||||
|
getPrefs(context).edit().putInt(KEY_CURRENT_INDEX, index).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChannelCount(): Int = channels.size
|
||||||
|
|
||||||
|
fun getChannel(index: Int): ChannelInfo? {
|
||||||
|
if (index in channels.indices) return channels[index]
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentChannel(): ChannelInfo? = getChannel(currentIndex)
|
||||||
|
|
||||||
|
fun nextChannel(): ChannelInfo? {
|
||||||
|
if (channels.isEmpty()) return null
|
||||||
|
currentIndex = (currentIndex + 1) % channels.size
|
||||||
|
return channels[currentIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prevChannel(): ChannelInfo? {
|
||||||
|
if (channels.isEmpty()) return null
|
||||||
|
currentIndex = if (currentIndex - 1 < 0) channels.size - 1 else currentIndex - 1
|
||||||
|
return channels[currentIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPrefs(context: Context): SharedPreferences {
|
||||||
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package cn.flyzxf.FengTv
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class ConfigDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private val filePickerLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
uri?.let { handleFileUri(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val items = arrayOf("输入 M3U8 源链接", "选择本地 TXT 文件", "取消")
|
||||||
|
|
||||||
|
return AlertDialog.Builder(requireActivity())
|
||||||
|
.setTitle("设置电视源")
|
||||||
|
.setItems(items) { _, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> showUrlInputDialog()
|
||||||
|
1 -> filePickerLauncher.launch(arrayOf("text/plain"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUrlInputDialog() {
|
||||||
|
// 提前捕获 activity 引用,防止回调时 Fragment 已被销毁导致 IllegalStateException
|
||||||
|
val activity = requireActivity() as MainActivity
|
||||||
|
val context = requireContext()
|
||||||
|
|
||||||
|
val input = EditText(context)
|
||||||
|
input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
|
||||||
|
input.hint = "https://example.com/channels.txt"
|
||||||
|
|
||||||
|
val layout = LinearLayout(context).apply {
|
||||||
|
setPadding(50, 20, 50, 20)
|
||||||
|
addView(input, LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(activity)
|
||||||
|
.setTitle("输入 M3U8 源链接")
|
||||||
|
.setView(layout)
|
||||||
|
.setPositiveButton("确定") { _, _ ->
|
||||||
|
val url = input.text.toString().trim()
|
||||||
|
if (url.isNotEmpty()) {
|
||||||
|
// 简单验证 URL 格式
|
||||||
|
if (!isValidUrl(url)) {
|
||||||
|
Toast.makeText(context, "链接格式不正确,请检查后重试", Toast.LENGTH_LONG).show()
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
activity.onConfigSaved(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidUrl(url: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val u = URL(url)
|
||||||
|
u.protocol == "http" || u.protocol == "https"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFileUri(uri: Uri) {
|
||||||
|
try {
|
||||||
|
// 提前捕获引用
|
||||||
|
val context = requireContext()
|
||||||
|
val activity = requireActivity() as MainActivity
|
||||||
|
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
BufferedReader(InputStreamReader(inputStream, "UTF-8")).use { reader ->
|
||||||
|
reader.forEachLine { lines.add(it) }
|
||||||
|
}
|
||||||
|
ChannelManager.saveChannels(context, lines)
|
||||||
|
activity.startPlayback()
|
||||||
|
Toast.makeText(context, "已加载本地文件", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 此时如果 Fragment 已分离,requireContext() 可能抛异常,用安全方式获取
|
||||||
|
try {
|
||||||
|
Toast.makeText(requireContext(), "读取文件失败", Toast.LENGTH_LONG).show()
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
// 忽略,Fragment 已销毁
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,592 @@
|
|||||||
package cn.flyzxf.FengTv
|
package cn.flyzxf.FengTv
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.PlaybackException
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var playerView: PlayerView
|
||||||
|
private lateinit var channelNameOverlay: TextView
|
||||||
|
private lateinit var loadingOverlay: View
|
||||||
|
private lateinit var loadingText: TextView
|
||||||
|
private lateinit var bottomToolbar: View
|
||||||
|
private lateinit var channelPanel: View
|
||||||
|
private lateinit var channelListView: ListView
|
||||||
|
private lateinit var toolbarIndicator: TextView
|
||||||
|
private lateinit var btnPrev: ImageButton
|
||||||
|
private lateinit var btnNext: ImageButton
|
||||||
|
private lateinit var btnToggleReverse: ImageButton
|
||||||
|
private lateinit var btnShowList: ImageButton
|
||||||
|
private lateinit var btnSettings: ImageButton
|
||||||
|
private lateinit var channelPanelClose: TextView
|
||||||
|
private lateinit var switchingIndicator: View
|
||||||
|
private lateinit var switchingChannelNum: TextView
|
||||||
|
private lateinit var switchingChannelTotal: TextView
|
||||||
|
private lateinit var switchingStatus: TextView
|
||||||
|
private var player: ExoPlayer? = null
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
private var retryCount = 0
|
||||||
|
private val maxRetries = 3
|
||||||
|
private var isFirstLaunch = true
|
||||||
|
private var hasShownNetworkRetryToast = false
|
||||||
|
private var isReverseOrder = false
|
||||||
|
private var isToolbarShowing = false
|
||||||
|
|
||||||
|
// 频道名消失 Runnable
|
||||||
|
private val hideChannelNameRunnable = Runnable {
|
||||||
|
channelNameOverlay.visibility = View.GONE
|
||||||
|
}
|
||||||
|
// 工具栏消失 Runnable
|
||||||
|
private val hideToolbarRunnable = Runnable {
|
||||||
|
hideToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_NAME = "fengtv_prefs"
|
||||||
|
private const val KEY_SOURCE_URL = "source_url"
|
||||||
|
private const val KEY_FIRST_LAUNCH = "first_launch"
|
||||||
|
private const val KEY_HAS_CONFIG = "has_config"
|
||||||
|
private const val KEY_REVERSE = "reverse_order"
|
||||||
|
private const val TIMEOUT_MS = 3000
|
||||||
|
private const val CHANNEL_NAME_DURATION_MS = 3000L
|
||||||
|
private const val NETWORK_DELAY_MS = 5000L
|
||||||
|
private const val TOOLBAR_DURATION_MS = 5000L
|
||||||
|
private const val LOADING_TIMEOUT_MS = 4000L
|
||||||
|
private const val SWITCH_STATUS_DURATION_MS = 2000L
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
playerView = findViewById(R.id.player_view)
|
||||||
|
channelNameOverlay = findViewById(R.id.channel_name_overlay)
|
||||||
|
loadingOverlay = findViewById(R.id.loading_overlay)
|
||||||
|
loadingText = findViewById(R.id.loading_text)
|
||||||
|
bottomToolbar = findViewById(R.id.bottom_toolbar)
|
||||||
|
channelPanel = findViewById(R.id.channel_panel)
|
||||||
|
channelListView = findViewById(R.id.channel_list_view)
|
||||||
|
toolbarIndicator = findViewById(R.id.toolbar_channel_indicator)
|
||||||
|
btnPrev = findViewById(R.id.btn_prev_channel)
|
||||||
|
btnNext = findViewById(R.id.btn_next_channel)
|
||||||
|
btnToggleReverse = findViewById(R.id.btn_toggle_reverse)
|
||||||
|
btnShowList = findViewById(R.id.btn_show_list)
|
||||||
|
btnSettings = findViewById(R.id.btn_settings)
|
||||||
|
channelPanelClose = findViewById(R.id.channel_panel_close)
|
||||||
|
switchingIndicator = findViewById(R.id.switching_indicator)
|
||||||
|
switchingChannelNum = findViewById(R.id.switching_channel_num)
|
||||||
|
switchingChannelTotal = findViewById(R.id.switching_channel_total)
|
||||||
|
switchingStatus = findViewById(R.id.switching_status)
|
||||||
|
|
||||||
|
playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT)
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 读取反转设置
|
||||||
|
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||||
|
isReverseOrder = prefs.getBoolean(KEY_REVERSE, false)
|
||||||
|
updateReverseButton()
|
||||||
|
|
||||||
|
registerNetworkCallback()
|
||||||
|
setupControls()
|
||||||
|
|
||||||
|
isFirstLaunch = prefs.getBoolean(KEY_FIRST_LAUNCH, true)
|
||||||
|
|
||||||
|
if (isFirstLaunch) {
|
||||||
|
prefs.edit().putBoolean(KEY_FIRST_LAUNCH, false).apply()
|
||||||
|
showConfigDialog()
|
||||||
|
} else {
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 控件绑定 ========================
|
||||||
|
|
||||||
|
private fun setupControls() {
|
||||||
|
// 点击画面切换工具栏
|
||||||
|
findViewById<View>(R.id.main).setOnClickListener {
|
||||||
|
toggleToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上一台
|
||||||
|
btnPrev.setOnClickListener { switchChannel(false) }
|
||||||
|
btnPrev.setOnLongClickListener {
|
||||||
|
showChannelPanel()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一台
|
||||||
|
btnNext.setOnClickListener { switchChannel(true) }
|
||||||
|
btnNext.setOnLongClickListener {
|
||||||
|
showChannelPanel()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 换台反转
|
||||||
|
btnToggleReverse.setOnClickListener {
|
||||||
|
isReverseOrder = !isReverseOrder
|
||||||
|
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||||
|
.edit().putBoolean(KEY_REVERSE, isReverseOrder).apply()
|
||||||
|
updateReverseButton()
|
||||||
|
Toast.makeText(this, if (isReverseOrder) "已反转换台方向" else "已恢复默认方向", Toast.LENGTH_SHORT).show()
|
||||||
|
refreshToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 频道列表
|
||||||
|
btnShowList.setOnClickListener { showChannelPanel() }
|
||||||
|
|
||||||
|
// 设置
|
||||||
|
btnSettings.setOnClickListener { showConfigDialog() }
|
||||||
|
|
||||||
|
// 频道面板关闭
|
||||||
|
channelPanelClose.setOnClickListener { hideChannelPanel() }
|
||||||
|
|
||||||
|
// 频道列表点击
|
||||||
|
channelListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
|
||||||
|
val channels = ChannelManager.loadChannels(this)
|
||||||
|
if (position in channels.indices) {
|
||||||
|
ChannelManager.saveCurrentIndex(this@MainActivity, position)
|
||||||
|
playChannel(channels[position])
|
||||||
|
hideChannelPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 工具栏 ========================
|
||||||
|
|
||||||
|
private fun toggleToolbar() {
|
||||||
|
if (channelPanel.visibility == View.VISIBLE) return
|
||||||
|
if (isToolbarShowing) {
|
||||||
|
hideToolbar()
|
||||||
|
} else {
|
||||||
|
showToolbar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToolbar() {
|
||||||
|
refreshToolbar()
|
||||||
|
bottomToolbar.visibility = View.VISIBLE
|
||||||
|
isToolbarShowing = true
|
||||||
|
mainHandler.removeCallbacks(hideToolbarRunnable)
|
||||||
|
mainHandler.postDelayed(hideToolbarRunnable, TOOLBAR_DURATION_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideToolbar() {
|
||||||
|
bottomToolbar.visibility = View.GONE
|
||||||
|
isToolbarShowing = false
|
||||||
|
mainHandler.removeCallbacks(hideToolbarRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshToolbar() {
|
||||||
|
val channel = ChannelManager.getCurrentChannel()
|
||||||
|
val total = ChannelManager.getChannelCount()
|
||||||
|
val num = ChannelManager.currentIndex + 1
|
||||||
|
toolbarIndicator.text = if (channel != null) "$num/$total ${channel.name}" else "无频道"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 频道列表面板 ========================
|
||||||
|
|
||||||
|
private fun showChannelPanel() {
|
||||||
|
hideToolbar()
|
||||||
|
val channels = ChannelManager.loadChannels(this)
|
||||||
|
if (channels.isEmpty()) {
|
||||||
|
Toast.makeText(this, "没有频道", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val names = channels.mapIndexed { index, ch ->
|
||||||
|
val prefix = if (index == ChannelManager.currentIndex) "▶ " else " "
|
||||||
|
"$prefix${index + 1}. ${ch.name}"
|
||||||
|
}
|
||||||
|
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, names)
|
||||||
|
channelListView.adapter = adapter
|
||||||
|
channelListView.setSelection(ChannelManager.currentIndex)
|
||||||
|
|
||||||
|
channelPanel.visibility = View.VISIBLE
|
||||||
|
channelListView.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideChannelPanel() {
|
||||||
|
channelPanel.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 反转按钮 ========================
|
||||||
|
|
||||||
|
private fun updateReverseButton() {
|
||||||
|
btnToggleReverse.alpha = if (isReverseOrder) 1.0f else 0.4f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 按键 & 触摸 ========================
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
return when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_CHANNEL_UP -> {
|
||||||
|
if (channelPanel.visibility == View.VISIBLE) {
|
||||||
|
event?.let { channelListView.dispatchKeyEvent(it) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switchChannel(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_CHANNEL_DOWN -> {
|
||||||
|
if (channelPanel.visibility == View.VISIBLE) {
|
||||||
|
event?.let { channelListView.dispatchKeyEvent(it) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switchChannel(false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER -> {
|
||||||
|
if (channelPanel.visibility == View.VISIBLE) {
|
||||||
|
// 选中当前项
|
||||||
|
val pos = channelListView.selectedItemPosition
|
||||||
|
if (pos >= 0) {
|
||||||
|
channelListView.performItemClick(null, pos, 0)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
showChannelName()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_BACK -> {
|
||||||
|
if (channelPanel.visibility == View.VISIBLE) {
|
||||||
|
hideChannelPanel()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (isToolbarShowing) {
|
||||||
|
hideToolbar()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_SETTINGS, KeyEvent.KEYCODE_MENU -> {
|
||||||
|
showConfigDialog()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 配置 ========================
|
||||||
|
|
||||||
|
private fun showConfigDialog() {
|
||||||
|
val configDialog = ConfigDialogFragment()
|
||||||
|
configDialog.show(supportFragmentManager, "config_dialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onConfigSaved(url: String) {
|
||||||
|
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||||
|
prefs.edit().putString(KEY_SOURCE_URL, url).putBoolean(KEY_HAS_CONFIG, true).apply()
|
||||||
|
ChannelManager.saveSourceUrl(this, url)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLocalFileSelected(filePath: String) {
|
||||||
|
try {
|
||||||
|
val file = File(filePath)
|
||||||
|
if (file.exists()) {
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
BufferedReader(FileReader(file)).use { reader ->
|
||||||
|
reader.forEachLine { lines.add(it) }
|
||||||
|
}
|
||||||
|
ChannelManager.saveChannels(this, lines)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, "读取文件失败: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 播放逻辑 ========================
|
||||||
|
|
||||||
|
fun startPlayback() {
|
||||||
|
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||||
|
val hasConfig = prefs.getBoolean(KEY_HAS_CONFIG, false)
|
||||||
|
|
||||||
|
if (!hasConfig) {
|
||||||
|
showConfigDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasShownNetworkRetryToast = false
|
||||||
|
|
||||||
|
val channels = ChannelManager.loadChannels(this)
|
||||||
|
if (channels.isNotEmpty()) {
|
||||||
|
playChannel(ChannelManager.getCurrentChannel()!!)
|
||||||
|
} else {
|
||||||
|
loadingOverlay.visibility = View.VISIBLE
|
||||||
|
loadingText.text = "正在加载频道列表..."
|
||||||
|
loadingText.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceUrl = ChannelManager.getSavedSourceUrl(this)
|
||||||
|
if (sourceUrl != null) {
|
||||||
|
executor.execute {
|
||||||
|
downloadAndUpdateSource(sourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadAndUpdateSource(sourceUrl: String) {
|
||||||
|
try {
|
||||||
|
val url = URL(sourceUrl)
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.connectTimeout = TIMEOUT_MS
|
||||||
|
conn.readTimeout = TIMEOUT_MS
|
||||||
|
conn.instanceFollowRedirects = true
|
||||||
|
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
BufferedReader(InputStreamReader(conn.inputStream, "UTF-8")).use { reader ->
|
||||||
|
reader.forEachLine { lines.add(it) }
|
||||||
|
}
|
||||||
|
if (lines.isNotEmpty()) {
|
||||||
|
val channelCountBefore = ChannelManager.getChannelCount()
|
||||||
|
ChannelManager.saveChannels(this, lines)
|
||||||
|
val newCount = ChannelManager.getChannelCount()
|
||||||
|
mainHandler.post {
|
||||||
|
if (channelCountBefore == 0 && newCount > 0) {
|
||||||
|
playChannel(ChannelManager.getCurrentChannel()!!)
|
||||||
|
} else {
|
||||||
|
showChannelName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(this@MainActivity, "下载源失败: HTTP $responseCode", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
if (ChannelManager.getChannelCount() == 0) {
|
||||||
|
if (!hasShownNetworkRetryToast) {
|
||||||
|
hasShownNetworkRetryToast = true
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(this@MainActivity, "无法连接服务器,将在后台继续尝试...", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainHandler.postDelayed({
|
||||||
|
executor.execute { downloadAndUpdateSource(sourceUrl) }
|
||||||
|
}, NETWORK_DELAY_MS)
|
||||||
|
} else {
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(this@MainActivity, "更新源失败,使用缓存", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载超时 Runnable
|
||||||
|
private var loadingTimeoutRunnable: Runnable? = null
|
||||||
|
|
||||||
|
private fun showSwitchingIndicator() {
|
||||||
|
val total = ChannelManager.getChannelCount()
|
||||||
|
val num = ChannelManager.currentIndex + 1
|
||||||
|
switchingChannelNum.text = "$num"
|
||||||
|
switchingChannelTotal.text = "/$total"
|
||||||
|
switchingStatus.text = ""
|
||||||
|
switchingIndicator.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSwitchingStatus(text: String) {
|
||||||
|
switchingStatus.text = text
|
||||||
|
switchingIndicator.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideSwitchingIndicator() {
|
||||||
|
switchingIndicator.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLoadingTimeout() {
|
||||||
|
cancelLoadingTimeout()
|
||||||
|
loadingTimeoutRunnable = Runnable {
|
||||||
|
// 加载超时,自动随机跳到另一个台(避免两个坏台间死循环)
|
||||||
|
showSwitchingStatus("超时,随机切换")
|
||||||
|
player?.stop()
|
||||||
|
mainHandler.postDelayed({
|
||||||
|
autoNextChannel()
|
||||||
|
}, 500L)
|
||||||
|
}
|
||||||
|
loadingTimeoutRunnable?.let {
|
||||||
|
mainHandler.postDelayed(it, LOADING_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelLoadingTimeout() {
|
||||||
|
loadingTimeoutRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||||
|
loadingTimeoutRunnable = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playChannel(channel: ChannelInfo) {
|
||||||
|
loadingOverlay.visibility = View.VISIBLE
|
||||||
|
loadingText.text = "加载中..."
|
||||||
|
loadingText.visibility = View.VISIBLE
|
||||||
|
retryCount = 0
|
||||||
|
hideToolbar()
|
||||||
|
|
||||||
|
// 显示左上角切换指示(频道序号)
|
||||||
|
showSwitchingIndicator()
|
||||||
|
|
||||||
|
// 启动加载超时定时器
|
||||||
|
startLoadingTimeout()
|
||||||
|
|
||||||
|
player?.release()
|
||||||
|
player = ExoPlayer.Builder(this).build().apply {
|
||||||
|
setMediaItem(MediaItem.fromUri(Uri.parse(channel.url)))
|
||||||
|
prepare()
|
||||||
|
playWhenReady = true
|
||||||
|
addListener(object : Player.Listener {
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
if (playbackState == Player.STATE_READY) {
|
||||||
|
cancelLoadingTimeout()
|
||||||
|
hideSwitchingIndicator()
|
||||||
|
loadingOverlay.visibility = View.GONE
|
||||||
|
loadingText.visibility = View.GONE
|
||||||
|
showChannelName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
|
cancelLoadingTimeout()
|
||||||
|
loadingOverlay.visibility = View.GONE
|
||||||
|
loadingText.visibility = View.GONE
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
retryCount++
|
||||||
|
showSwitchingStatus("重试 $retryCount/$maxRetries")
|
||||||
|
mainHandler.postDelayed({
|
||||||
|
player?.seekTo(0)
|
||||||
|
player?.prepare()
|
||||||
|
// 重新设定加载超时(重试也算)
|
||||||
|
startLoadingTimeout()
|
||||||
|
}, 1000L)
|
||||||
|
} else {
|
||||||
|
showSwitchingStatus("加载失败,随机切换")
|
||||||
|
mainHandler.postDelayed({
|
||||||
|
autoNextChannel()
|
||||||
|
}, 800L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
playerView.player = player
|
||||||
|
ChannelManager.saveCurrentIndex(this, ChannelManager.currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchChannel(next: Boolean) {
|
||||||
|
val channel = if (isReverseOrder xor next) {
|
||||||
|
ChannelManager.prevChannel()
|
||||||
|
} else {
|
||||||
|
ChannelManager.nextChannel()
|
||||||
|
}
|
||||||
|
if (channel != null) {
|
||||||
|
playChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun autoNextChannel() {
|
||||||
|
val channels = ChannelManager.loadChannels(this)
|
||||||
|
if (channels.isEmpty()) return
|
||||||
|
if (channels.size == 1) {
|
||||||
|
playChannel(channels[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 随机跳到另一个台,避免在两个坏台间无限循环
|
||||||
|
var nextIndex: Int
|
||||||
|
do {
|
||||||
|
nextIndex = (0 until channels.size).random()
|
||||||
|
} while (nextIndex == ChannelManager.currentIndex && channels.size > 1)
|
||||||
|
|
||||||
|
ChannelManager.saveCurrentIndex(this, nextIndex)
|
||||||
|
ChannelManager.getChannel(nextIndex)?.let { playChannel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showChannelName() {
|
||||||
|
val channel = ChannelManager.getCurrentChannel()
|
||||||
|
if (channel != null) {
|
||||||
|
val total = ChannelManager.getChannelCount()
|
||||||
|
val num = ChannelManager.currentIndex + 1
|
||||||
|
channelNameOverlay.text = "[$num/$total] " + channel.name
|
||||||
|
channelNameOverlay.visibility = View.VISIBLE
|
||||||
|
mainHandler.removeCallbacks(hideChannelNameRunnable)
|
||||||
|
mainHandler.postDelayed(hideChannelNameRunnable, CHANNEL_NAME_DURATION_MS)
|
||||||
|
refreshToolbar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 网络回调 ========================
|
||||||
|
|
||||||
|
private fun registerNetworkCallback() {
|
||||||
|
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val builder = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
connectivityManager.registerNetworkCallback(builder.build(), object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
val sourceUrl = ChannelManager.getSavedSourceUrl(this@MainActivity)
|
||||||
|
if (sourceUrl != null && ChannelManager.getChannelCount() > 0) {
|
||||||
|
executor.execute {
|
||||||
|
downloadAndUpdateSource(sourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 生命周期 ========================
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
player?.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
player?.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
player?.release()
|
||||||
|
executor.shutdown()
|
||||||
|
mainHandler.removeCallbacksAndMessages(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,170 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
<!-- 纯深蓝色背景 -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#3DDC84"
|
android:fillColor="#0D47A1"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
<!-- 背景色(深蓝圆角方块) -->
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:pathData="M14,24 L94,24 Q100,24 100,30 L100,86 Q100,92 94,92 L14,92 Q8,92 8,86 L8,30 Q8,24 14,24 Z"
|
||||||
android:fillType="nonZero"
|
android:fillColor="#1565C0" />
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
<!-- 屏幕高光(内部梯形) -->
|
||||||
android:strokeWidth="1"
|
<path
|
||||||
android:strokeColor="#00000000" />
|
android:pathData="M15,28 L93,28 L93,76 L15,76 Z"
|
||||||
|
android:fillColor="#1976D2" />
|
||||||
|
<!-- 屏幕浅色区域 -->
|
||||||
|
<path
|
||||||
|
android:pathData="M18,32 L90,32 Q91,32 91,33 L91,74 Q91,75 90,75 L18,75 Q17,75 17,74 L17,33 Q17,32 18,32 Z"
|
||||||
|
android:fillColor="#42A5F5" />
|
||||||
|
<!-- 播放按钮(白色三角) -->
|
||||||
|
<path
|
||||||
|
android:pathData="M42,42 L42,66 L68,54 Z"
|
||||||
|
android:fillColor="#FFFFFF" />
|
||||||
|
<!-- 底座 -->
|
||||||
|
<path
|
||||||
|
android:pathData="M44,92 L64,92 L64,98 L44,98 Z"
|
||||||
|
android:fillColor="#1565C0" />
|
||||||
|
<!-- 底座阴影 -->
|
||||||
|
<path
|
||||||
|
android:pathData="M42,98 L66,98 L66,100 L42,100 Z"
|
||||||
|
android:fillColor="#0D47A1" />
|
||||||
</vector>
|
</vector>
|
||||||
@@ -1,19 +1,232 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity">
|
android:keepScreenOn="true">
|
||||||
|
|
||||||
|
<!-- 视频播放器(隐藏默认控制器) -->
|
||||||
|
<androidx.media3.ui.PlayerView
|
||||||
|
android:id="@+id/player_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/black"
|
||||||
|
app:use_controller="false" />
|
||||||
|
|
||||||
|
<!-- 右上角频道名 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/channel_name_overlay"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:background="#88000000"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<!-- 左上角切换指示(频道序号 + 状态) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/switching_indicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|start"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/switching_channel_num"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="#FFFF00"
|
||||||
|
android:textSize="56sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/switching_channel_total"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:paddingTop="20dp"
|
||||||
|
android:textColor="#FFFF00"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/switching_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:paddingTop="18dp"
|
||||||
|
android:textColor="#FFCC00"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 加载中遮罩 -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/loading_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#FF000000"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Hello World!"
|
android:layout_gravity="center"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:text="加载中..."
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:textColor="#FFFFFF"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:textSize="18sp"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
android:id="@+id/loading_text"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<!-- ============ 频道列表覆盖层 ============ -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/channel_panel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#CC000000"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:background="#88000000"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:text="频道列表"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/channel_panel_close"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:text="✕ 关闭"
|
||||||
|
android:textColor="#FFAAAA"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/channel_list_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="#00000000"
|
||||||
|
android:cacheColorHint="#00000000"
|
||||||
|
android:divider="#33FFFFFF"
|
||||||
|
android:dividerHeight="1dp"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- ============ 底部工具栏 ============ -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bottom_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="#CC000000"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<!-- 上一台 -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev_channel"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@android:drawable/ic_media_previous"
|
||||||
|
android:contentDescription="上一台" />
|
||||||
|
|
||||||
|
<!-- 下一台 -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next_channel"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@android:drawable/ic_media_next"
|
||||||
|
android:contentDescription="下一台" />
|
||||||
|
|
||||||
|
<!-- 间隔 -->
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<!-- 频道指示器 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/toolbar_channel_indicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<!-- 换台反转 -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_toggle_reverse"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@android:drawable/ic_menu_sort_by_size"
|
||||||
|
android:contentDescription="换台反转" />
|
||||||
|
|
||||||
|
<!-- 频道列表 -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_show_list"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@android:drawable/ic_menu_agenda"
|
||||||
|
android:contentDescription="频道列表" />
|
||||||
|
|
||||||
|
<!-- 设置 -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_settings"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@android:drawable/ic_menu_manage"
|
||||||
|
android:contentDescription="设置" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Base application theme. -->
|
<resources>
|
||||||
<style name="Base.Theme.FengTv" parent="Theme.Material3.DayNight.NoActionBar">
|
<style name="Theme.FengTv" parent="Theme.AppCompat.NoActionBar">
|
||||||
<!-- Customize your dark theme here. -->
|
<item name="android:windowFullscreen">true</item>
|
||||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">FengTv</string>
|
<string name="app_name">FengTV</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Base application theme. -->
|
<resources>
|
||||||
<style name="Base.Theme.FengTv" parent="Theme.Material3.DayNight.NoActionBar">
|
<style name="Theme.FengTv" parent="Theme.AppCompat.NoActionBar">
|
||||||
<!-- Customize your light theme here. -->
|
<item name="android:windowFullscreen">true</item>
|
||||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.FengTv" parent="Base.Theme.FengTv" />
|
|
||||||
</resources>
|
</resources>
|
||||||
+3
-18
@@ -1,23 +1,8 @@
|
|||||||
# Project-wide Gradle settings.
|
# Project-wide Gradle settings.
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
org.gradle.java.home=C\:/Program Files/Java/jdk-17
|
||||||
# This option should only be used with decoupled projects. For more details, visit
|
|
||||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app's APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
systemProp.gradle.wrapperUser=
|
||||||
|
systemProp.gradle.wrapperPassword=
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.4.0-rc02"
|
agp = "8.4.0"
|
||||||
kotlin = "1.9.0"
|
kotlin = "1.9.22"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.1.5"
|
junitVersion = "1.1.5"
|
||||||
@@ -9,6 +9,7 @@ appcompat = "1.6.1"
|
|||||||
material = "1.10.0"
|
material = "1.10.0"
|
||||||
activity = "1.8.0"
|
activity = "1.8.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
|
media3 = "1.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -19,8 +20,10 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
|
|||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
|
||||||
|
androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" }
|
||||||
|
androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
#Wed Jun 17 14:18:24 CST 2026
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.6-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
|
maven { setUrl("https://maven.aliyun.com/repository/public") }
|
||||||
|
maven { setUrl("https://maven.aliyun.com/repository/google") }
|
||||||
|
maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
|
||||||
google {
|
google {
|
||||||
content {
|
content {
|
||||||
includeGroupByRegex("com\\.android.*")
|
includeGroupByRegex("com\\.android.*")
|
||||||
@@ -14,6 +17,8 @@ pluginManagement {
|
|||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
|
maven { setUrl("https://maven.aliyun.com/repository/public") }
|
||||||
|
maven { setUrl("https://maven.aliyun.com/repository/google") }
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user