Files
android-tv/app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt

389 lines
14 KiB
Kotlin
Raw Normal View History

package com.videoapp.tv.ui
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
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
import kotlinx.coroutines.launch
class SearchFragment : Fragment() {
private lateinit var searchInput: EditText
private lateinit var btnSearch: Button
private lateinit var btnSettings: Button
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
private lateinit var fallbackWebView: WebView
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) },
onItemFocus = { view -> view.animate().scaleX(1.05f).scaleY(1.05f).setDuration(150).start() }
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_search, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
searchInput = view.findViewById(R.id.search_input)
btnSearch = view.findViewById(R.id.btn_search)
btnSettings = view.findViewById(R.id.btn_settings)
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)
fallbackWebView = view.findViewById(R.id.fallback_webview)
setupResultsGrid()
setupListeners()
loadHistory()
loadPlayHistory()
}
override fun onResume() {
super.onResume()
// 每次返回首页时刷新播放历史
loadPlayHistory()
}
private fun setupResultsGrid() {
val spanCount = if (resources.configuration.screenWidthDp >= 600) 5 else 3
resultsGrid.layoutManager = GridLayoutManager(context, spanCount)
resultsGrid.adapter = adapter
}
private fun setupListeners() {
searchInput.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH ||
(event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)
) {
performSearch(searchInput.text.toString().trim())
true
} else {
false
}
}
btnSearch.setOnClickListener {
performSearch(searchInput.text.toString().trim())
}
btnSettings.setOnClickListener {
startActivity(Intent(requireContext(), SettingsActivity::class.java))
}
btnClearHistory.setOnClickListener {
lifecycleScope.launch {
historyDao.clearAll()
showHistory(emptyList())
}
}
btnClearPlayHistory.setOnClickListener {
lifecycleScope.launch {
playHistoryDao.clearAll()
showPlayHistory(emptyList())
}
}
}
private fun performSearch(keyword: String) {
if (keyword.isEmpty()) return
hideFallbackWebView()
showLoading(true)
statusText.visibility = View.GONE
historyContainer.visibility = View.GONE
playHistoryContainer.visibility = View.GONE
lifecycleScope.launch {
// Save to history
val existing = historyDao.findByKeyword(keyword)
if (existing != null) {
historyDao.updateTime(existing.id, System.currentTimeMillis())
} else {
historyDao.insert(SearchHistory(keyword = keyword))
}
}
lifecycleScope.launch {
searchCoordinator.search(
keyword = keyword,
onResult = { results ->
showLoading(false)
adapter.setItems(results)
if (results.isEmpty()) {
showStatus("未找到结果")
}
},
onError = { error ->
showLoading(false)
if (error == "FALLBACK_WEBVIEW") {
showFallbackWebView(keyword)
} else {
showStatus("搜索失败,请重试")
Toast.makeText(requireContext(), error, Toast.LENGTH_LONG).show()
}
},
onFallbackToWebView = {
showFallbackWebView(keyword)
}
)
}
}
private fun showFallbackWebView(keyword: String) {
val config = searchCoordinator.getConfig()
val base = config.baseUrl.trimEnd('/')
val path = config.searchPath.trimStart('/')
val params = "${config.keywordParam}=${java.net.URLEncoder.encode(keyword, "UTF-8")}"
val extraParams = config.extraParams.entries.joinToString("&") { "${it.key}=${it.value}" }
val url = "$base/$path?$params&$extraParams"
fallbackWebView.visibility = View.VISIBLE
resultsGrid.visibility = View.GONE
fallbackWebView.settings.javaScriptEnabled = true
fallbackWebView.webViewClient = WebViewClient()
fallbackWebView.loadUrl(url)
}
private fun hideFallbackWebView() {
fallbackWebView.visibility = View.GONE
fallbackWebView.stopLoading()
resultsGrid.visibility = View.VISIBLE
}
private fun showLoading(show: Boolean) {
loadingProgress.visibility = if (show) View.VISIBLE else View.GONE
}
private fun showStatus(msg: String) {
statusText.text = msg
statusText.visibility = View.VISIBLE
}
private fun openPlayer(result: SearchResult) {
val intent = Intent(requireContext(), PlayerActivity::class.java).apply {
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)
putExtra("history_episode", playHistory.episodeName)
}
startActivity(intent)
}
private fun loadHistory() {
lifecycleScope.launch {
historyDao.getRecentHistory().collect { list ->
showHistory(list)
}
}
}
private fun loadPlayHistory() {
lifecycleScope.launch {
playHistoryDao.getRecentPlayHistory().collect { list ->
showPlayHistory(list)
}
}
}
private fun showHistory(list: List<SearchHistory>) {
historyList.removeAllViews()
if (list.isEmpty()) {
historyContainer.visibility = View.GONE
return
}
for (item in list) {
val chip = Button(requireContext()).apply {
text = item.keyword
setTextColor(ContextCompat.getColor(requireContext(), R.color.text_primary))
setBackgroundResource(R.drawable.history_chip_selector)
textSize = 14f
setPadding(24, 8, 24, 8)
isFocusable = true
isFocusableInTouchMode = true
setOnClickListener {
searchInput.setText(item.keyword)
performSearch(item.keyword)
}
}
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 container = android.widget.LinearLayout(requireContext()).apply {
orientation = android.widget.LinearLayout.VERTICAL
setPadding(12, 12, 12, 12)
layoutParams = android.widget.LinearLayout.LayoutParams(
200, // 固定宽度 200dp
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
marginEnd = 16
}
background = android.graphics.drawable.GradientDrawable().apply {
setColor(android.graphics.Color.parseColor("#2D2D2D"))
cornerRadius = 12f
setStroke(2, android.graphics.Color.parseColor("#3D3D3D"))
}
}
// 创建封面图片区域
val coverView = android.widget.ImageView(requireContext()).apply {
layoutParams = android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
100 // 固定高度 100dp
).apply {
bottomMargin = 8
}
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
setBackgroundColor(android.graphics.Color.parseColor("#1E1E1E"))
// 加载封面图
if (!item.coverUrl.isNullOrEmpty()) {
com.bumptech.glide.Glide.with(this)
.load(item.coverUrl)
.placeholder(android.R.color.darker_gray)
.error(android.R.color.darker_gray)
.into(this)
}
}
// 创建文字显示区域
val titleText = TextView(requireContext()).apply {
text = item.title
setTextColor(ContextCompat.getColor(requireContext(), R.color.text_primary))
textSize = 13f
setPadding(4, 4, 4, 2)
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
}
// 创建剧集文字显示
val episodeText = TextView(requireContext()).apply {
text = item.episodeName ?: ""
setTextColor(ContextCompat.getColor(requireContext(), R.color.text_secondary))
textSize = 11f
setPadding(4, 2, 4, 4)
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
visibility = if (item.episodeName.isNullOrEmpty()) View.GONE else View.VISIBLE
}
// 添加到容器
container.addView(coverView)
container.addView(titleText)
container.addView(episodeText)
// 设置点击事件
container.setOnClickListener {
openPlayerFromHistory(item)
}
// 设置焦点效果
container.isFocusable = true
container.isFocusableInTouchMode = true
container.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
v.background = android.graphics.drawable.GradientDrawable().apply {
setColor(android.graphics.Color.parseColor("#3D3D3D"))
cornerRadius = 12f
setStroke(3, android.graphics.Color.parseColor("#1A73E8"))
}
v.animate().scaleX(1.05f).scaleY(1.05f).setDuration(150).start()
} else {
v.background = android.graphics.drawable.GradientDrawable().apply {
setColor(android.graphics.Color.parseColor("#2D2D2D"))
cornerRadius = 12f
setStroke(2, android.graphics.Color.parseColor("#3D3D3D"))
}
v.animate().scaleX(1.0f).scaleY(1.0f).setDuration(150).start()
}
}
playHistoryList.addView(container)
}
}
override fun onDestroyView() {
super.onDestroyView()
fallbackWebView.destroy()
}
}