389 lines
14 KiB
Kotlin
389 lines
14 KiB
Kotlin
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()
|
|
}
|
|
}
|