修改应用名称为夏季TV,添加播放历史功能,修复搜索框高度问题

This commit is contained in:
xiaji
2026-05-24 22:14:50 +08:00
parent cb016c116f
commit bbec29382d
8 changed files with 276 additions and 16 deletions

View File

@@ -15,7 +15,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.videoapp.tv.data.AppDatabase
import com.videoapp.tv.data.ConfigRepository
import com.videoapp.tv.data.PlayHistory
import com.videoapp.tv.engine.Episode
import com.videoapp.tv.engine.PlaySource
import com.videoapp.tv.engine.VideoExtractor
@@ -35,6 +37,7 @@ class PlayerActivity : AppCompatActivity() {
private var exoPlayer: ExoPlayer? = null
private val videoExtractor = VideoExtractor()
private val configRepo by lazy { ConfigRepository(this) }
private val playHistoryDao by lazy { AppDatabase.getInstance(this).playHistoryDao() }
private var sources: List<PlaySource> = emptyList()
private var currentSourceIndex = 0
@@ -44,6 +47,12 @@ class PlayerActivity : AppCompatActivity() {
private val hideRunnable = Runnable { hideControls() }
private var controlsVisible = true
// 用于保存播放历史的信息
private var videoTitle: String = ""
private var videoCategory: String? = null
private var coverUrl: String? = null
private var detailUrl: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
@@ -57,7 +66,10 @@ class PlayerActivity : AppCompatActivity() {
errorText = findViewById(R.id.error_text)
btnClose = findViewById(R.id.btn_close)
val detailUrl = intent.getStringExtra("detail_url") ?: ""
detailUrl = intent.getStringExtra("detail_url") ?: ""
videoTitle = intent.getStringExtra("title") ?: ""
videoCategory = intent.getStringExtra("category")
coverUrl = intent.getStringExtra("cover_url")
btnClose.setOnClickListener { finish() }
@@ -161,6 +173,9 @@ class PlayerActivity : AppCompatActivity() {
showLoading(true)
val config = configRepo.getConfig()
// 保存播放历史
savePlayHistory(ep.title)
lifecycleScope.launch {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(ep.playUrl, config)
@@ -174,7 +189,37 @@ class PlayerActivity : AppCompatActivity() {
}
}
private fun savePlayHistory(episodeName: String?) {
lifecycleScope.launch {
try {
// 检查是否已存在相同的播放记录
val existing = playHistoryDao.findByDetailUrl(detailUrl)
if (existing != null) {
// 更新现有记录的播放时间和剧集
playHistoryDao.updatePlayTime(existing.id, System.currentTimeMillis(), episodeName)
} else {
// 插入新记录
val playHistory = PlayHistory(
title = videoTitle,
episodeName = episodeName,
detailUrl = detailUrl,
coverUrl = coverUrl,
category = videoCategory,
playTime = System.currentTimeMillis()
)
playHistoryDao.insert(playHistory)
}
} catch (e: Exception) {
// 保存播放历史失败不影响播放功能
e.printStackTrace()
}
}
}
private fun tryPlayDirectly(detailUrl: String, config: com.videoapp.tv.data.SiteConfig) {
// 保存播放历史(直接播放时使用标题作为剧集名)
savePlayHistory(videoTitle)
lifecycleScope.launch {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
if (directUrl != null) {

View File

@@ -5,9 +5,10 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [SearchHistory::class], version = 1, exportSchema = false)
@Database(entities = [SearchHistory::class, PlayHistory::class], version = 2, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun searchHistoryDao(): SearchHistoryDao
abstract fun playHistoryDao(): PlayHistoryDao
companion object {
@Volatile
@@ -19,7 +20,9 @@ abstract class AppDatabase : RoomDatabase() {
context.applicationContext,
AppDatabase::class.java,
"video_search_tv.db"
).build()
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}

View File

@@ -0,0 +1,15 @@
package com.videoapp.tv.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "play_history")
data class PlayHistory(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val episodeName: String? = null,
val detailUrl: String,
val coverUrl: String? = null,
val category: String? = null,
val playTime: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,29 @@
package com.videoapp.tv.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface PlayHistoryDao {
@Query("SELECT * FROM play_history ORDER BY playTime DESC LIMIT 20")
fun getRecentPlayHistory(): Flow<List<PlayHistory>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(playHistory: PlayHistory)
@Query("DELETE FROM play_history WHERE detailUrl = :detailUrl")
suspend fun deleteByDetailUrl(detailUrl: String)
@Query("DELETE FROM play_history")
suspend fun clearAll()
@Query("SELECT * FROM play_history WHERE detailUrl = :detailUrl LIMIT 1")
suspend fun findByDetailUrl(detailUrl: String): PlayHistory?
@Query("UPDATE play_history SET playTime = :playTime, episodeName = :episodeName WHERE id = :id")
suspend fun updatePlayTime(id: Long, playTime: Long, episodeName: String?)
}

View File

@@ -22,6 +22,7 @@ import com.videoapp.tv.R
import com.videoapp.tv.PlayerActivity
import com.videoapp.tv.SettingsActivity
import com.videoapp.tv.data.AppDatabase
import com.videoapp.tv.data.PlayHistory
import com.videoapp.tv.data.SearchHistory
import com.videoapp.tv.data.SearchResult
import com.videoapp.tv.engine.SearchCoordinator
@@ -35,6 +36,10 @@ class SearchFragment : Fragment() {
private lateinit var historyContainer: ViewGroup
private lateinit var historyList: ViewGroup
private lateinit var btnClearHistory: Button
private lateinit var playHistoryContainer: ViewGroup
private lateinit var playHistoryList: ViewGroup
private lateinit var btnClearPlayHistory: Button
private lateinit var emptyPlayHistory: TextView
private lateinit var resultsGrid: RecyclerView
private lateinit var loadingProgress: View
private lateinit var statusText: TextView
@@ -42,6 +47,7 @@ class SearchFragment : Fragment() {
private val searchCoordinator by lazy { SearchCoordinator(requireContext()) }
private val historyDao by lazy { AppDatabase.getInstance(requireContext()).searchHistoryDao() }
private val playHistoryDao by lazy { AppDatabase.getInstance(requireContext()).playHistoryDao() }
private val adapter by lazy {
SearchResultAdapter(
onItemClick = { result -> openPlayer(result) },
@@ -66,6 +72,10 @@ class SearchFragment : Fragment() {
historyContainer = view.findViewById(R.id.history_container)
historyList = view.findViewById(R.id.history_list)
btnClearHistory = view.findViewById(R.id.btn_clear_history)
playHistoryContainer = view.findViewById(R.id.play_history_container)
playHistoryList = view.findViewById(R.id.play_history_list)
btnClearPlayHistory = view.findViewById(R.id.btn_clear_play_history)
emptyPlayHistory = view.findViewById(R.id.empty_play_history)
resultsGrid = view.findViewById(R.id.results_grid)
loadingProgress = view.findViewById(R.id.loading_progress)
statusText = view.findViewById(R.id.status_text)
@@ -74,6 +84,13 @@ class SearchFragment : Fragment() {
setupResultsGrid()
setupListeners()
loadHistory()
loadPlayHistory()
}
override fun onResume() {
super.onResume()
// 每次返回首页时刷新播放历史
loadPlayHistory()
}
private fun setupResultsGrid() {
@@ -108,6 +125,13 @@ class SearchFragment : Fragment() {
showHistory(emptyList())
}
}
btnClearPlayHistory.setOnClickListener {
lifecycleScope.launch {
playHistoryDao.clearAll()
showPlayHistory(emptyList())
}
}
}
private fun performSearch(keyword: String) {
@@ -117,6 +141,7 @@ class SearchFragment : Fragment() {
showLoading(true)
statusText.visibility = View.GONE
historyContainer.visibility = View.GONE
playHistoryContainer.visibility = View.GONE
lifecycleScope.launch {
// Save to history
@@ -189,6 +214,17 @@ class SearchFragment : Fragment() {
putExtra("detail_url", result.detailUrl)
putExtra("title", result.title)
putExtra("category", result.category)
putExtra("cover_url", result.coverUrl)
}
startActivity(intent)
}
private fun openPlayerFromHistory(playHistory: PlayHistory) {
val intent = Intent(requireContext(), PlayerActivity::class.java).apply {
putExtra("detail_url", playHistory.detailUrl)
putExtra("title", playHistory.title)
putExtra("category", playHistory.category)
putExtra("cover_url", playHistory.coverUrl)
}
startActivity(intent)
}
@@ -201,6 +237,14 @@ class SearchFragment : Fragment() {
}
}
private fun loadPlayHistory() {
lifecycleScope.launch {
playHistoryDao.getRecentPlayHistory().collect { list ->
showPlayHistory(list)
}
}
}
private fun showHistory(list: List<SearchHistory>) {
historyList.removeAllViews()
if (list.isEmpty()) {
@@ -224,6 +268,44 @@ class SearchFragment : Fragment() {
}
historyList.addView(chip)
}
historyContainer.visibility = View.VISIBLE
}
private fun showPlayHistory(list: List<PlayHistory>) {
playHistoryList.removeAllViews()
if (list.isEmpty()) {
// 显示空状态提示
emptyPlayHistory.visibility = View.VISIBLE
btnClearPlayHistory.visibility = View.GONE
playHistoryList.visibility = View.GONE
return
}
// 有播放历史,隐藏空状态提示
emptyPlayHistory.visibility = View.GONE
btnClearPlayHistory.visibility = View.VISIBLE
playHistoryList.visibility = View.VISIBLE
for (item in list) {
val chip = Button(requireContext()).apply {
// 显示视频名称和剧集名称
text = if (item.episodeName != null) {
"${item.title} - ${item.episodeName}"
} else {
item.title
}
setTextColor(ContextCompat.getColor(requireContext(), R.color.text_primary))
setBackgroundResource(R.drawable.history_chip_selector)
textSize = 14f
setPadding(24, 12, 24, 12)
isFocusable = true
isFocusableInTouchMode = true
setOnClickListener {
openPlayerFromHistory(item)
}
}
playHistoryList.addView(chip)
}
}
override fun onDestroyView() {

View File

@@ -9,7 +9,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface"
android:padding="16dp">
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
@@ -19,36 +19,116 @@
<EditText
android:id="@+id/search_input"
style="@style/SearchEditText"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_height="64dp"
android:layout_weight="1"
android:hint="@string/search_hint" />
android:background="@drawable/search_bg"
android:hint="@string/search_hint"
android:textColor="@color/text_primary"
android:textColorHint="@color/text_secondary"
android:textSize="20sp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginEnd="12dp"
android:singleLine="true"
android:imeOptions="actionSearch"
android:inputType="text"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_vertical"
android:minHeight="64dp" />
<Button
android:id="@+id/btn_search"
android:layout_width="56dp"
android:layout_height="48dp"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
android:background="@drawable/button_bg"
android:focusable="true"
android:text="搜"
android:textColor="@color/text_primary"
android:textSize="16sp" />
android:textSize="18sp" />
<Button
android:id="@+id/btn_settings"
android:layout_width="56dp"
android:layout_height="48dp"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@drawable/episode_bg"
android:focusable="true"
android:text="⚙"
android:textColor="@color/text_primary"
android:textSize="16sp" />
android:textSize="18sp" />
</LinearLayout>
</FrameLayout>
<!-- 播放历史区域 -->
<LinearLayout
android:id="@+id/play_history_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/play_history_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/play_history"
android:textColor="@color/text_secondary"
android:textSize="14sp"
android:padding="8dp" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/btn_clear_play_history"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:background="@drawable/history_chip_bg"
android:focusable="true"
android:text="@string/clear_history"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
<HorizontalScrollView
android:id="@+id/play_history_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:id="@+id/play_history_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</HorizontalScrollView>
<TextView
android:id="@+id/empty_play_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="暂无播放记录,搜索并观看视频后将显示在这里"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:padding="16dp"
android:gravity="center" />
</LinearLayout>
<!-- 搜索历史区域 -->
<LinearLayout
android:id="@+id/history_container"
android:layout_width="match_parent"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">视频搜索TV</string>
<string name="app_name">夏季TV</string>
<string name="search_hint">输入关键词搜索...</string>
<string name="search_button">搜索</string>
<string name="clear_history">清空历史</string>
@@ -17,6 +17,7 @@
<string name="restore_default">恢复默认</string>
<string name="saved">已保存</string>
<string name="history">搜索历史</string>
<string name="play_history">播放历史</string>
<string name="select_episode">选择剧集</string>
<string name="movie">电影</string>
<string name="tv_series">电视剧</string>

View File

@@ -16,18 +16,23 @@
<item name="android:navigationBarColor">@android:color/black</item>
</style>
<!-- 搜索框样式 -->
<style name="SearchEditText" parent="Widget.AppCompat.EditText">
<item name="android:background">@drawable/search_bg</item>
<item name="android:textColor">@color/text_primary</item>
<item name="android:textColorHint">@color/text_secondary</item>
<item name="android:textSize">18sp</item>
<item name="android:padding">16dp</item>
<item name="android:layout_margin">16dp</item>
<item name="android:paddingStart">16dp</item>
<item name="android:paddingEnd">16dp</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="android:layout_margin">8dp</item>
<item name="android:singleLine">true</item>
<item name="android:imeOptions">actionSearch</item>
<item name="android:inputType">text</item>
<item name="android:focusable">true</item>
<item name="android:focusableInTouchMode">true</item>
<item name="android:gravity">center_vertical</item>
</style>
<style name="HistoryChip">