diff --git a/app/src/main/java/com/videoapp/tv/PlayerActivity.kt b/app/src/main/java/com/videoapp/tv/PlayerActivity.kt index 21df949..c945bd1 100644 --- a/app/src/main/java/com/videoapp/tv/PlayerActivity.kt +++ b/app/src/main/java/com/videoapp/tv/PlayerActivity.kt @@ -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 = 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) { diff --git a/app/src/main/java/com/videoapp/tv/data/AppDatabase.kt b/app/src/main/java/com/videoapp/tv/data/AppDatabase.kt index bbe4d8a..2ecce7c 100644 --- a/app/src/main/java/com/videoapp/tv/data/AppDatabase.kt +++ b/app/src/main/java/com/videoapp/tv/data/AppDatabase.kt @@ -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 } diff --git a/app/src/main/java/com/videoapp/tv/data/PlayHistory.kt b/app/src/main/java/com/videoapp/tv/data/PlayHistory.kt new file mode 100644 index 0000000..82d3875 --- /dev/null +++ b/app/src/main/java/com/videoapp/tv/data/PlayHistory.kt @@ -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() +) diff --git a/app/src/main/java/com/videoapp/tv/data/PlayHistoryDao.kt b/app/src/main/java/com/videoapp/tv/data/PlayHistoryDao.kt new file mode 100644 index 0000000..373bcaa --- /dev/null +++ b/app/src/main/java/com/videoapp/tv/data/PlayHistoryDao.kt @@ -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> + + @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?) +} diff --git a/app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt b/app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt index a43970f..b10b7c2 100644 --- a/app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt +++ b/app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt @@ -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) { historyList.removeAllViews() if (list.isEmpty()) { @@ -224,6 +268,44 @@ class SearchFragment : Fragment() { } historyList.addView(chip) } + historyContainer.visibility = View.VISIBLE + } + + private fun showPlayHistory(list: List) { + 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() { diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 2200f28..d9cf0f1 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -9,7 +9,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/surface" - android:padding="16dp"> + android:padding="12dp"> + 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" />