修改应用名称为夏季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.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.videoapp.tv.data.AppDatabase
import com.videoapp.tv.data.ConfigRepository import com.videoapp.tv.data.ConfigRepository
import com.videoapp.tv.data.PlayHistory
import com.videoapp.tv.engine.Episode import com.videoapp.tv.engine.Episode
import com.videoapp.tv.engine.PlaySource import com.videoapp.tv.engine.PlaySource
import com.videoapp.tv.engine.VideoExtractor import com.videoapp.tv.engine.VideoExtractor
@@ -35,6 +37,7 @@ class PlayerActivity : AppCompatActivity() {
private var exoPlayer: ExoPlayer? = null private var exoPlayer: ExoPlayer? = null
private val videoExtractor = VideoExtractor() private val videoExtractor = VideoExtractor()
private val configRepo by lazy { ConfigRepository(this) } private val configRepo by lazy { ConfigRepository(this) }
private val playHistoryDao by lazy { AppDatabase.getInstance(this).playHistoryDao() }
private var sources: List<PlaySource> = emptyList() private var sources: List<PlaySource> = emptyList()
private var currentSourceIndex = 0 private var currentSourceIndex = 0
@@ -44,6 +47,12 @@ class PlayerActivity : AppCompatActivity() {
private val hideRunnable = Runnable { hideControls() } private val hideRunnable = Runnable { hideControls() }
private var controlsVisible = true 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player) setContentView(R.layout.activity_player)
@@ -57,7 +66,10 @@ class PlayerActivity : AppCompatActivity() {
errorText = findViewById(R.id.error_text) errorText = findViewById(R.id.error_text)
btnClose = findViewById(R.id.btn_close) 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() } btnClose.setOnClickListener { finish() }
@@ -161,6 +173,9 @@ class PlayerActivity : AppCompatActivity() {
showLoading(true) showLoading(true)
val config = configRepo.getConfig() val config = configRepo.getConfig()
// 保存播放历史
savePlayHistory(ep.title)
lifecycleScope.launch { lifecycleScope.launch {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(ep.playUrl, config) 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) { private fun tryPlayDirectly(detailUrl: String, config: com.videoapp.tv.data.SiteConfig) {
// 保存播放历史(直接播放时使用标题作为剧集名)
savePlayHistory(videoTitle)
lifecycleScope.launch { lifecycleScope.launch {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config) val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
if (directUrl != null) { if (directUrl != null) {

View File

@@ -5,9 +5,10 @@ import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase 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 class AppDatabase : RoomDatabase() {
abstract fun searchHistoryDao(): SearchHistoryDao abstract fun searchHistoryDao(): SearchHistoryDao
abstract fun playHistoryDao(): PlayHistoryDao
companion object { companion object {
@Volatile @Volatile
@@ -19,7 +20,9 @@ abstract class AppDatabase : RoomDatabase() {
context.applicationContext, context.applicationContext,
AppDatabase::class.java, AppDatabase::class.java,
"video_search_tv.db" "video_search_tv.db"
).build() )
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance INSTANCE = 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.PlayerActivity
import com.videoapp.tv.SettingsActivity import com.videoapp.tv.SettingsActivity
import com.videoapp.tv.data.AppDatabase import com.videoapp.tv.data.AppDatabase
import com.videoapp.tv.data.PlayHistory
import com.videoapp.tv.data.SearchHistory import com.videoapp.tv.data.SearchHistory
import com.videoapp.tv.data.SearchResult import com.videoapp.tv.data.SearchResult
import com.videoapp.tv.engine.SearchCoordinator import com.videoapp.tv.engine.SearchCoordinator
@@ -35,6 +36,10 @@ class SearchFragment : Fragment() {
private lateinit var historyContainer: ViewGroup private lateinit var historyContainer: ViewGroup
private lateinit var historyList: ViewGroup private lateinit var historyList: ViewGroup
private lateinit var btnClearHistory: Button 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 resultsGrid: RecyclerView
private lateinit var loadingProgress: View private lateinit var loadingProgress: View
private lateinit var statusText: TextView private lateinit var statusText: TextView
@@ -42,6 +47,7 @@ class SearchFragment : Fragment() {
private val searchCoordinator by lazy { SearchCoordinator(requireContext()) } private val searchCoordinator by lazy { SearchCoordinator(requireContext()) }
private val historyDao by lazy { AppDatabase.getInstance(requireContext()).searchHistoryDao() } private val historyDao by lazy { AppDatabase.getInstance(requireContext()).searchHistoryDao() }
private val playHistoryDao by lazy { AppDatabase.getInstance(requireContext()).playHistoryDao() }
private val adapter by lazy { private val adapter by lazy {
SearchResultAdapter( SearchResultAdapter(
onItemClick = { result -> openPlayer(result) }, onItemClick = { result -> openPlayer(result) },
@@ -66,6 +72,10 @@ class SearchFragment : Fragment() {
historyContainer = view.findViewById(R.id.history_container) historyContainer = view.findViewById(R.id.history_container)
historyList = view.findViewById(R.id.history_list) historyList = view.findViewById(R.id.history_list)
btnClearHistory = view.findViewById(R.id.btn_clear_history) 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) resultsGrid = view.findViewById(R.id.results_grid)
loadingProgress = view.findViewById(R.id.loading_progress) loadingProgress = view.findViewById(R.id.loading_progress)
statusText = view.findViewById(R.id.status_text) statusText = view.findViewById(R.id.status_text)
@@ -74,6 +84,13 @@ class SearchFragment : Fragment() {
setupResultsGrid() setupResultsGrid()
setupListeners() setupListeners()
loadHistory() loadHistory()
loadPlayHistory()
}
override fun onResume() {
super.onResume()
// 每次返回首页时刷新播放历史
loadPlayHistory()
} }
private fun setupResultsGrid() { private fun setupResultsGrid() {
@@ -108,6 +125,13 @@ class SearchFragment : Fragment() {
showHistory(emptyList()) showHistory(emptyList())
} }
} }
btnClearPlayHistory.setOnClickListener {
lifecycleScope.launch {
playHistoryDao.clearAll()
showPlayHistory(emptyList())
}
}
} }
private fun performSearch(keyword: String) { private fun performSearch(keyword: String) {
@@ -117,6 +141,7 @@ class SearchFragment : Fragment() {
showLoading(true) showLoading(true)
statusText.visibility = View.GONE statusText.visibility = View.GONE
historyContainer.visibility = View.GONE historyContainer.visibility = View.GONE
playHistoryContainer.visibility = View.GONE
lifecycleScope.launch { lifecycleScope.launch {
// Save to history // Save to history
@@ -189,6 +214,17 @@ class SearchFragment : Fragment() {
putExtra("detail_url", result.detailUrl) putExtra("detail_url", result.detailUrl)
putExtra("title", result.title) putExtra("title", result.title)
putExtra("category", result.category) 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) 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>) { private fun showHistory(list: List<SearchHistory>) {
historyList.removeAllViews() historyList.removeAllViews()
if (list.isEmpty()) { if (list.isEmpty()) {
@@ -224,6 +268,44 @@ class SearchFragment : Fragment() {
} }
historyList.addView(chip) 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() { override fun onDestroyView() {

View File

@@ -9,7 +9,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/surface" android:background="@color/surface"
android:padding="16dp"> android:padding="12dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -19,36 +19,116 @@
<EditText <EditText
android:id="@+id/search_input" android:id="@+id/search_input"
style="@style/SearchEditText"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="48dp" android:layout_height="64dp"
android:layout_weight="1" 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 <Button
android:id="@+id/btn_search" android:id="@+id/btn_search"
android:layout_width="56dp" android:layout_width="64dp"
android:layout_height="48dp" android:layout_height="64dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:background="@drawable/button_bg" android:background="@drawable/button_bg"
android:focusable="true" android:focusable="true"
android:text="搜" android:text="搜"
android:textColor="@color/text_primary" android:textColor="@color/text_primary"
android:textSize="16sp" /> android:textSize="18sp" />
<Button <Button
android:id="@+id/btn_settings" android:id="@+id/btn_settings"
android:layout_width="56dp" android:layout_width="64dp"
android:layout_height="48dp" android:layout_height="64dp"
android:background="@drawable/episode_bg" android:background="@drawable/episode_bg"
android:focusable="true" android:focusable="true"
android:text="⚙" android:text="⚙"
android:textColor="@color/text_primary" android:textColor="@color/text_primary"
android:textSize="16sp" /> android:textSize="18sp" />
</LinearLayout> </LinearLayout>
</FrameLayout> </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 <LinearLayout
android:id="@+id/history_container" android:id="@+id/history_container"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

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

View File

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