修改应用名称为夏季TV,添加播放历史功能,修复搜索框高度问题
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
15
app/src/main/java/com/videoapp/tv/data/PlayHistory.kt
Normal file
15
app/src/main/java/com/videoapp/tv/data/PlayHistory.kt
Normal 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()
|
||||
)
|
||||
29
app/src/main/java/com/videoapp/tv/data/PlayHistoryDao.kt
Normal file
29
app/src/main/java/com/videoapp/tv/data/PlayHistoryDao.kt
Normal 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?)
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user