Fix episode parsing, add missing categories, improve search stability
- Add old_ent (추억의 예능) and old_drama (추억의 드라마) categories - Fix episode parsing to prioritize #epListScroll selector - Add pagination logic to fetch more episodes when 25+ episodes - Fix extractSeriesUrl to include old_ent and old_drama - Add crash protection in search with try-catch - Add showSearchDialog to MainActivity for better search UX - Fix SearchResultsAdapter to use Presenter pattern correctly - Remove unused scale variable in SearchCardPresenter
This commit is contained in:
@@ -59,8 +59,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/search_title"
|
android:label="@string/search_title"
|
||||||
android:screenOrientation="landscape"
|
android:screenOrientation="landscape"
|
||||||
android:theme="@style/Theme.Tvmon.Search"
|
android:theme="@style/Theme.Tvmon.Search">
|
||||||
android:launchMode="singleTop">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class TvmonScraper {
|
|||||||
const val BASE_URL = "https://tvmon.site"
|
const val BASE_URL = "https://tvmon.site"
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
val CATEGORIES = mapOf(
|
val CATEGORIES = mapOf(
|
||||||
"popular" to Category("popular", "인기영상", "/popular"),
|
"popular" to Category("popular", "인기영상", "/popular"),
|
||||||
"movie" to Category("movie", "영화", "/movie"),
|
"movie" to Category("movie", "영화", "/movie"),
|
||||||
"kor_movie" to Category("kor_movie", "한국영화", "/kor_movie"),
|
"kor_movie" to Category("kor_movie", "한국영화", "/kor_movie"),
|
||||||
@@ -25,7 +25,9 @@ class TvmonScraper {
|
|||||||
"world" to Category("world", "해외드라마", "/world"),
|
"world" to Category("world", "해외드라마", "/world"),
|
||||||
"ott_ent" to Category("ott_ent", "해외 (예능/다큐)", "/ott_ent"),
|
"ott_ent" to Category("ott_ent", "해외 (예능/다큐)", "/ott_ent"),
|
||||||
"ani_movie" to Category("ani_movie", "[극장판] 애니메이션", "/ani_movie"),
|
"ani_movie" to Category("ani_movie", "[극장판] 애니메이션", "/ani_movie"),
|
||||||
"animation" to Category("animation", "일반 애니메이션", "/animation")
|
"animation" to Category("animation", "일반 애니메이션", "/animation"),
|
||||||
|
"old_ent" to Category("old_ent", "추억의 예능", "/old_ent"),
|
||||||
|
"old_drama" to Category("old_drama", "추억의 드라마", "/old_drama")
|
||||||
)
|
)
|
||||||
|
|
||||||
private val NAV_PATTERNS = listOf("/login", "/logout", "/register", "/mypage", "/bbs/", "/menu", "/faq", "/privacy")
|
private val NAV_PATTERNS = listOf("/login", "/logout", "/register", "/mypage", "/bbs/", "/menu", "/faq", "/privacy")
|
||||||
@@ -61,8 +63,8 @@ class TvmonScraper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractSeriesUrl(url: String): String {
|
private fun extractSeriesUrl(url: String): String {
|
||||||
val pattern = Pattern.compile("(/(drama|movie|ent|world|animation|kor_movie|sisa|ott_ent|ani_movie)/\\d+)/\\d+")
|
val pattern = Pattern.compile("(/(drama|movie|ent|world|animation|kor_movie|sisa|ott_ent|ani_movie|old_ent|old_drama)/\\d+)/\\d+")
|
||||||
val matcher = pattern.matcher(url)
|
val matcher = pattern.matcher(url)
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
return BASE_URL + matcher.group(1)
|
return BASE_URL + matcher.group(1)
|
||||||
@@ -407,13 +409,19 @@ class TvmonScraper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = mutableListOf<Episode>()
|
||||||
val videoLinks = mutableListOf<VideoLink>()
|
val videoLinks = mutableListOf<VideoLink>()
|
||||||
val seenEpisodeIds = mutableSetOf<String>()
|
val seenEpisodeIds = mutableSetOf<String>()
|
||||||
|
|
||||||
val seriesId = seriesUrl.substringAfterLast("/").substringBefore("?")
|
val seriesId = seriesUrl.substringAfterLast("/").substringBefore("?")
|
||||||
|
|
||||||
val allEpisodeLinks = doc.select("a[href*='/$seriesId/'], a[href*='/${seriesId}/']")
|
// Parse episodes from #epListScroll first (ep-item class)
|
||||||
|
val epListScroll = doc.select("#epListScroll")
|
||||||
|
val allEpisodeLinks = if (epListScroll.isNotEmpty()) {
|
||||||
|
epListScroll.select("a.ep-item[href*='/$seriesId/']")
|
||||||
|
} else {
|
||||||
|
doc.select("a[href*='/$seriesId/'], a[href*='/${seriesId}/']")
|
||||||
|
}
|
||||||
|
|
||||||
var episodeIndex = 0
|
var episodeIndex = 0
|
||||||
|
|
||||||
@@ -493,6 +501,62 @@ class TvmonScraper {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have 25+ episodes, try to fetch more episodes from the series list page
|
||||||
|
if (episodes.size >= 25) {
|
||||||
|
val category = getCategoryFromUrl(seriesUrl)
|
||||||
|
val listPageUrl = "$BASE_URL/$category/$seriesId"
|
||||||
|
val listHtml = get(listPageUrl)
|
||||||
|
if (listHtml != null) {
|
||||||
|
val listDoc = Jsoup.parse(listHtml)
|
||||||
|
val listEpLinks = listDoc.select("a[href*='/$seriesId/']")
|
||||||
|
for (link in listEpLinks) {
|
||||||
|
val href = link.attr("href")
|
||||||
|
if (href.isBlank()) continue
|
||||||
|
|
||||||
|
val fullUrl = resolveUrl(href)
|
||||||
|
if (seenEpisodeIds.contains(fullUrl)) continue
|
||||||
|
|
||||||
|
val episodeIdMatch = Pattern.compile("/$seriesId/(\\d+)").matcher(href)
|
||||||
|
if (!episodeIdMatch.find()) continue
|
||||||
|
val episodeId = episodeIdMatch.group(1) ?: ""
|
||||||
|
if (episodeId in seenEpisodeIds) continue
|
||||||
|
seenEpisodeIds.add(episodeId)
|
||||||
|
|
||||||
|
val linkText = link.text().trim()
|
||||||
|
if (linkText.isBlank()) continue
|
||||||
|
if (linkText.contains("로그인") || linkText.contains("비밀번호") ||
|
||||||
|
linkText.contains("마이페이지") || linkText.contains("전체 목록")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val cleanLinkText = linkText.replace("시청중", "").replace("NEW", "").trim()
|
||||||
|
|
||||||
|
val episodeNumMatch = Pattern.compile("(\\d+)\\s*화|(\\d+)\\s*회|EP\\.?(\\d+)|제\\s*(\\d+)\\s*부").matcher(cleanLinkText)
|
||||||
|
val episodeTitle = if (episodeNumMatch.find()) {
|
||||||
|
episodeNumMatch.group(1) ?: episodeNumMatch.group(2) ?: episodeNumMatch.group(3) ?: episodeNumMatch.group(4)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalNumber = episodeTitle ?: (episodes.size + 1).toString()
|
||||||
|
|
||||||
|
episodes.add(Episode(
|
||||||
|
number = finalNumber,
|
||||||
|
title = cleanLinkText.ifBlank { finalNumber },
|
||||||
|
url = fullUrl,
|
||||||
|
type = "webview",
|
||||||
|
date = ""
|
||||||
|
))
|
||||||
|
|
||||||
|
videoLinks.add(VideoLink(
|
||||||
|
type = "play_page",
|
||||||
|
url = fullUrl,
|
||||||
|
title = linkText
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
episodes.sortByDescending { episode ->
|
episodes.sortByDescending { episode ->
|
||||||
val numberStr = episode.number
|
val numberStr = episode.number
|
||||||
val pattern = Pattern.compile("\\d+")
|
val pattern = Pattern.compile("\\d+")
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
package com.example.tvmon.ui.main
|
package com.example.tvmon.ui.main
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.example.tvmon.R
|
import com.example.tvmon.R
|
||||||
|
|
||||||
@@ -10,4 +20,107 @@ class MainActivity : FragmentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSearchDialog(onSearch: (String) -> Unit) {
|
||||||
|
val layout = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(60, 40, 60, 40)
|
||||||
|
setBackgroundColor(android.graphics.Color.parseColor("#1A1A1A"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val titleText = TextView(this).apply {
|
||||||
|
text = "검색"
|
||||||
|
textSize = 28f
|
||||||
|
setTextColor(android.graphics.Color.WHITE)
|
||||||
|
setPadding(0, 0, 0, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
val editText = EditText(this).apply {
|
||||||
|
hint = "검색어를 입력하세요"
|
||||||
|
textSize = 22f
|
||||||
|
setTextColor(android.graphics.Color.WHITE)
|
||||||
|
setHintTextColor(android.graphics.Color.GRAY)
|
||||||
|
setBackgroundColor(android.graphics.Color.parseColor("#333333"))
|
||||||
|
setPadding(30, 25, 30, 25)
|
||||||
|
inputType = InputType.TYPE_CLASS_TEXT
|
||||||
|
imeOptions = EditorInfo.IME_ACTION_SEARCH
|
||||||
|
isFocusable = true
|
||||||
|
isFocusableInTouchMode = true
|
||||||
|
requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
val buttonLayout = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
setPadding(0, 30, 0, 0)
|
||||||
|
gravity = android.view.Gravity.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
val cancelBtn = Button(this).apply {
|
||||||
|
text = "취소"
|
||||||
|
textSize = 18f
|
||||||
|
setBackgroundColor(android.graphics.Color.parseColor("#555555"))
|
||||||
|
setTextColor(android.graphics.Color.WHITE)
|
||||||
|
setPadding(40, 20, 40, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchBtn = Button(this).apply {
|
||||||
|
text = "검색"
|
||||||
|
textSize = 18f
|
||||||
|
setBackgroundColor(android.graphics.Color.parseColor("#E50914"))
|
||||||
|
setTextColor(android.graphics.Color.WHITE)
|
||||||
|
setPadding(40, 20, 40, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonLayout.addView(cancelBtn)
|
||||||
|
buttonLayout.addView(searchBtn)
|
||||||
|
|
||||||
|
layout.addView(titleText)
|
||||||
|
layout.addView(editText)
|
||||||
|
layout.addView(buttonLayout)
|
||||||
|
|
||||||
|
val dialog = Dialog(this, android.R.style.Theme_Translucent_NoTitleBar)
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
dialog.setContentView(layout)
|
||||||
|
|
||||||
|
dialog.window?.setLayout(
|
||||||
|
(resources.displayMetrics.widthPixels * 0.8).toInt(),
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
dialog.window?.setBackgroundDrawableResource(android.R.color.black)
|
||||||
|
|
||||||
|
cancelBtn.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBtn.setOnClickListener {
|
||||||
|
val query = editText.text.toString().trim()
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
onSearch(query)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editText.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
val query = editText.text.toString().trim()
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
onSearch(query)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
editText.post {
|
||||||
|
editText.requestFocus()
|
||||||
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,14 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
|
|||||||
onItemViewClickedListener = this
|
onItemViewClickedListener = this
|
||||||
onItemViewSelectedListener = this
|
onItemViewSelectedListener = this
|
||||||
|
|
||||||
setOnSearchClickedListener {
|
setOnSearchClickedListener {
|
||||||
startActivity(Intent(requireContext(), com.example.tvmon.ui.search.SearchActivity::class.java))
|
val activity = requireActivity() as? MainActivity
|
||||||
|
activity?.showSearchDialog { query ->
|
||||||
|
val intent = Intent(requireContext(), com.example.tvmon.ui.search.SearchActivity::class.java).apply {
|
||||||
|
putExtra("initial_query", query)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Log.w(TAG, "setupUI: UI setup complete, adapter set")
|
Log.w(TAG, "setupUI: UI setup complete, adapter set")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.tvmon.ui.presenter
|
package com.example.tvmon.ui.presenter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.leanback.widget.ImageCardView
|
import androidx.leanback.widget.ImageCardView
|
||||||
import androidx.leanback.widget.Presenter
|
import androidx.leanback.widget.Presenter
|
||||||
@@ -50,6 +51,69 @@ class ContentCardPresenter : Presenter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SearchCardPresenter : Presenter() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
||||||
|
val cardView = ImageCardView(parent.context).apply {
|
||||||
|
isFocusable = true
|
||||||
|
isFocusableInTouchMode = true
|
||||||
|
setBackgroundColor(parent.context.getColor(R.color.default_background))
|
||||||
|
cardType = ImageCardView.CARD_TYPE_INFO_UNDER
|
||||||
|
infoVisibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
cardView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
v.animate()
|
||||||
|
.scaleX(1.1f)
|
||||||
|
.scaleY(1.1f)
|
||||||
|
.translationZ(10f)
|
||||||
|
.setDuration(100)
|
||||||
|
.start()
|
||||||
|
} else {
|
||||||
|
v.animate()
|
||||||
|
.scaleX(1.0f)
|
||||||
|
.scaleY(1.0f)
|
||||||
|
.translationZ(0f)
|
||||||
|
.setDuration(100)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewHolder(cardView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
||||||
|
val content = item as Content
|
||||||
|
val cardView = viewHolder.view as ImageCardView
|
||||||
|
val res = cardView.context.resources
|
||||||
|
|
||||||
|
cardView.titleText = content.title
|
||||||
|
cardView.contentText = content.category
|
||||||
|
|
||||||
|
val width = res.getDimensionPixelSize(R.dimen.card_width)
|
||||||
|
val height = res.getDimensionPixelSize(R.dimen.card_height)
|
||||||
|
cardView.setMainImageDimensions(width, height)
|
||||||
|
|
||||||
|
if (content.thumbnail.isNotBlank()) {
|
||||||
|
Glide.with(cardView.context)
|
||||||
|
.load(content.thumbnail)
|
||||||
|
.error(R.drawable.default_background)
|
||||||
|
.placeholder(R.drawable.default_background)
|
||||||
|
.centerCrop()
|
||||||
|
.into(cardView.mainImageView)
|
||||||
|
} else {
|
||||||
|
cardView.mainImageView.setImageResource(R.drawable.default_background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
|
||||||
|
val cardView = viewHolder.view as ImageCardView
|
||||||
|
cardView.badgeImage = null
|
||||||
|
cardView.mainImage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CategoryCardPresenter : Presenter() {
|
class CategoryCardPresenter : Presenter() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ package com.example.tvmon.ui.search
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.tvmon.R
|
import com.example.tvmon.R
|
||||||
import com.example.tvmon.data.model.Content
|
import com.example.tvmon.data.model.Content
|
||||||
import com.example.tvmon.data.scraper.TvmonScraper
|
import com.example.tvmon.data.scraper.TvmonScraper
|
||||||
import com.example.tvmon.ui.detail.DetailsActivity
|
import com.example.tvmon.ui.detail.DetailsActivity
|
||||||
|
import com.example.tvmon.ui.presenter.SearchCardPresenter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "TVMON_SEARCH"
|
private const val TAG = "TVMON_SEARCH"
|
||||||
|
private const val NUM_COLUMNS = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
private val scraper: TvmonScraper by inject()
|
private val scraper: TvmonScraper by inject()
|
||||||
@@ -30,6 +33,11 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_search)
|
setContentView(R.layout.activity_search)
|
||||||
|
|
||||||
setupUI()
|
setupUI()
|
||||||
|
|
||||||
|
val initialQuery = intent.getStringExtra("initial_query")
|
||||||
|
if (!initialQuery.isNullOrBlank()) {
|
||||||
|
searchView.setQuery(initialQuery, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
@@ -40,8 +48,9 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
openDetail(content)
|
openDetail(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
recyclerView.layoutManager = GridLayoutManager(this, NUM_COLUMNS)
|
||||||
recyclerView.adapter = adapter
|
recyclerView.adapter = adapter
|
||||||
|
recyclerView.setHasFixedSize(true)
|
||||||
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
@@ -68,10 +77,18 @@ class SearchActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun search(query: String) {
|
private fun search(query: String) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
val result = scraper.search(query)
|
val result = scraper.search(query)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (result.success) {
|
if (result.success && result.results.isNotEmpty()) {
|
||||||
adapter.updateResults(result.results)
|
adapter.updateResults(result.results)
|
||||||
|
} else {
|
||||||
|
adapter.updateResults(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
runOnUiThread {
|
||||||
|
adapter.updateResults(emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
package com.example.tvmon.ui.search
|
package com.example.tvmon.ui.search
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import androidx.leanback.widget.Presenter
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.example.tvmon.R
|
|
||||||
import com.example.tvmon.data.model.Content
|
import com.example.tvmon.data.model.Content
|
||||||
|
import com.example.tvmon.ui.presenter.SearchCardPresenter
|
||||||
|
|
||||||
class SearchResultsAdapter(
|
class SearchResultsAdapter(
|
||||||
private val onItemClick: (Content) -> Unit
|
private val onItemClick: (Content) -> Unit
|
||||||
) : RecyclerView.Adapter<SearchResultsAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<SearchResultsAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private val presenter = SearchCardPresenter()
|
||||||
private var results = mutableListOf<Content>()
|
private var results = mutableListOf<Content>()
|
||||||
|
|
||||||
fun updateResults(newResults: List<Content>) {
|
fun updateResults(newResults: List<Content>) {
|
||||||
@@ -23,39 +20,18 @@ class SearchResultsAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context)
|
return ViewHolder(presenter.onCreateViewHolder(parent))
|
||||||
.inflate(R.layout.item_search_result, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val content = results[position]
|
val content = results[position]
|
||||||
holder.bind(content, onItemClick)
|
presenter.onBindViewHolder(holder.viewHolder, content)
|
||||||
|
holder.viewHolder.view.setOnClickListener { onItemClick(content) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = results.size
|
override fun getItemCount(): Int = results.size
|
||||||
|
|
||||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
class ViewHolder(val viewHolder: Presenter.ViewHolder) : RecyclerView.ViewHolder(viewHolder.view)
|
||||||
private val thumbnailView: ImageView = view.findViewById(R.id.thumbnail)
|
|
||||||
private val titleView: TextView = view.findViewById(R.id.title)
|
|
||||||
private val categoryView: TextView = view.findViewById(R.id.category)
|
|
||||||
|
|
||||||
fun bind(content: Content, onClick: (Content) -> Unit) {
|
|
||||||
titleView.text = content.title
|
|
||||||
categoryView.text = content.category
|
|
||||||
|
|
||||||
if (content.thumbnail.isNotBlank()) {
|
|
||||||
Glide.with(itemView.context)
|
|
||||||
.load(content.thumbnail)
|
|
||||||
.placeholder(R.drawable.default_background)
|
|
||||||
.error(R.drawable.default_background)
|
|
||||||
.centerCrop()
|
|
||||||
.into(thumbnailView)
|
|
||||||
} else {
|
|
||||||
thumbnailView.setImageResource(R.drawable.default_background)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemView.setOnClickListener { onClick(content) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user