2026-05-24 21:09:05 +08:00
|
|
|
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
|
2026-05-24 21:14:19 +08:00
|
|
|
import androidx.core.content.ContextCompat
|
2026-05-24 21:09:05 +08:00
|
|
|
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.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 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 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)
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun setupResultsGrid() {
|
2026-05-24 21:19:34 +08:00
|
|
|
val spanCount = if (resources.configuration.screenWidthDp >= 600) 5 else 3
|
|
|
|
|
resultsGrid.layoutManager = GridLayoutManager(context, spanCount)
|
2026-05-24 21:09:05 +08:00
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun performSearch(keyword: String) {
|
|
|
|
|
if (keyword.isEmpty()) return
|
|
|
|
|
|
|
|
|
|
hideFallbackWebView()
|
|
|
|
|
showLoading(true)
|
|
|
|
|
statusText.visibility = View.GONE
|
|
|
|
|
historyContainer.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)
|
|
|
|
|
}
|
|
|
|
|
startActivity(intent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun loadHistory() {
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
|
historyDao.getRecentHistory().collect { list ->
|
|
|
|
|
showHistory(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
|
2026-05-24 21:14:19 +08:00
|
|
|
setTextColor(ContextCompat.getColor(requireContext(), R.color.text_primary))
|
2026-05-24 21:09:05 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDestroyView() {
|
|
|
|
|
super.onDestroyView()
|
|
|
|
|
fallbackWebView.destroy()
|
|
|
|
|
}
|
|
|
|
|
}
|