diff --git a/tvmon-app/app/src/main/AndroidManifest.xml b/tvmon-app/app/src/main/AndroidManifest.xml
index c68c3be..c39d08b 100644
--- a/tvmon-app/app/src/main/AndroidManifest.xml
+++ b/tvmon-app/app/src/main/AndroidManifest.xml
@@ -55,19 +55,18 @@
android:theme="@style/Theme.Tvmon.Search" />
-
-
-
-
-
+ android:name=".ui.search.SearchActivity"
+ android:exported="true"
+ android:label="@string/search_title"
+ android:screenOrientation="landscape"
+ android:theme="@style/Theme.Tvmon.Search">
+
+
+
+
+
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt b/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt
index e08d3e6..bcfb2cd 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt
@@ -15,18 +15,20 @@ class TvmonScraper {
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"
- val CATEGORIES = mapOf(
- "popular" to Category("popular", "인기영상", "/popular"),
- "movie" to Category("movie", "영화", "/movie"),
- "kor_movie" to Category("kor_movie", "한국영화", "/kor_movie"),
- "drama" to Category("drama", "드라마", "/drama"),
- "ent" to Category("ent", "예능프로그램", "/ent"),
- "sisa" to Category("sisa", "시사/다큐", "/sisa"),
- "world" to Category("world", "해외드라마", "/world"),
- "ott_ent" to Category("ott_ent", "해외 (예능/다큐)", "/ott_ent"),
- "ani_movie" to Category("ani_movie", "[극장판] 애니메이션", "/ani_movie"),
- "animation" to Category("animation", "일반 애니메이션", "/animation")
- )
+val CATEGORIES = mapOf(
+ "popular" to Category("popular", "인기영상", "/popular"),
+ "movie" to Category("movie", "영화", "/movie"),
+ "kor_movie" to Category("kor_movie", "한국영화", "/kor_movie"),
+ "drama" to Category("drama", "드라마", "/drama"),
+ "ent" to Category("ent", "예능프로그램", "/ent"),
+ "sisa" to Category("sisa", "시사/다큐", "/sisa"),
+ "world" to Category("world", "해외드라마", "/world"),
+ "ott_ent" to Category("ott_ent", "해외 (예능/다큐)", "/ott_ent"),
+ "ani_movie" to Category("ani_movie", "[극장판] 애니메이션", "/ani_movie"),
+ "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")
}
@@ -61,14 +63,14 @@ class TvmonScraper {
}
}
- 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 matcher = pattern.matcher(url)
- if (matcher.find()) {
- return BASE_URL + matcher.group(1)
- }
- return url
+private fun extractSeriesUrl(url: String): String {
+ 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)
+ if (matcher.find()) {
+ return BASE_URL + matcher.group(1)
}
+ return url
+ }
suspend fun getHomepage(): Map = withContext(Dispatchers.IO) {
val html = get("$BASE_URL/") ?: return@withContext mapOf("success" to false)
@@ -407,91 +409,153 @@ class TvmonScraper {
}
}
- val episodes = mutableListOf()
- val videoLinks = mutableListOf()
- val seenEpisodeIds = mutableSetOf()
+val episodes = mutableListOf()
+ val videoLinks = mutableListOf()
+ val seenEpisodeIds = mutableSetOf()
- 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
- for (link in allEpisodeLinks) {
- val href = link.attr("href")
- if (href.isBlank()) continue
- if (!href.contains("/$seriesId/") && !href.contains("/${seriesId}/")) continue
+ for (link in allEpisodeLinks) {
+ val href = link.attr("href")
+ if (href.isBlank()) continue
+ if (!href.contains("/$seriesId/") && !href.contains("/${seriesId}/")) continue
- val fullUrl = resolveUrl(href)
+ val fullUrl = resolveUrl(href)
- val episodeIdMatch = Pattern.compile("/$seriesId/(\\d+)").matcher(href)
- if (!episodeIdMatch.find()) 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 episodeId = episodeIdMatch.group(1) ?: ""
+ if (episodeId in seenEpisodeIds) continue
+ seenEpisodeIds.add(episodeId)
- val linkText = link.text().trim()
- if (linkText.isBlank()) continue
+ val linkText = link.text().trim()
+ if (linkText.isBlank()) continue
- if (linkText.contains("로그인") || linkText.contains("비밀번호") ||
- linkText.contains("마이페이지") || linkText.contains("전체 목록")) {
- continue
- }
+ if (linkText.contains("로그인") || linkText.contains("비밀번호") ||
+ linkText.contains("마이페이지") || linkText.contains("전체 목록")) {
+ continue
+ }
- val cleanLinkText = linkText
- .replace("시청중", "")
- .replace("NEW", "")
- .trim()
+ val cleanLinkText = linkText
+ .replace("시청중", "")
+ .replace("NEW", "")
+ .trim()
- val datePattern = Pattern.compile("^(\\d{2})[./](\\d{2})[./](\\d{2,4})\\s+(.+)$|^(\\d{2})[./](\\d{2})[./](\\d{2,4})(.+)$")
- val dateMatch = datePattern.matcher(linkText)
- var dateStr = ""
- var episodeTitleStr = cleanLinkText
-
- if (dateMatch.find()) {
- val day = dateMatch.group(1) ?: dateMatch.group(5)
- val month = dateMatch.group(2) ?: dateMatch.group(6)
- val year = dateMatch.group(3) ?: dateMatch.group(7)
- val titlePart = dateMatch.group(4) ?: dateMatch.group(8)
-
- if (day != null && month != null && year != null) {
- dateStr = if (year.length == 2) {
- "20$year/$month/$day"
- } else {
- "$year/$month/$day"
- }
- episodeTitleStr = titlePart?.trim() ?: cleanLinkText
- }
- }
+ val datePattern = Pattern.compile("^(\\d{2})[./](\\d{2})[./](\\d{2,4})\\s+(.+)$|^(\\d{2})[./](\\d{2})[./](\\d{2,4})(.+)$")
+ val dateMatch = datePattern.matcher(linkText)
+ var dateStr = ""
+ var episodeTitleStr = cleanLinkText
- val episodeNumMatch = Pattern.compile("(\\d+)\\s*화|(\\d+)\\s*회|EP\\.?(\\d+)|제\\s*(\\d+)\\s*부").matcher(episodeTitleStr)
- val episodeTitle = if (episodeNumMatch.find()) {
- episodeNumMatch.group(1) ?: episodeNumMatch.group(2) ?: episodeNumMatch.group(3) ?: episodeNumMatch.group(4)
- } else {
- null
- }
+ if (dateMatch.find()) {
+ val day = dateMatch.group(1) ?: dateMatch.group(5)
+ val month = dateMatch.group(2) ?: dateMatch.group(6)
+ val year = dateMatch.group(3) ?: dateMatch.group(7)
+ val titlePart = dateMatch.group(4) ?: dateMatch.group(8)
- val finalNumber = if (!episodeTitle.isNullOrBlank()) {
- episodeTitle
- } else {
- episodeIndex++.toString()
- }
-
- episodes.add(Episode(
- number = dateStr.ifBlank { finalNumber },
- title = episodeTitleStr.ifBlank { finalNumber },
- url = fullUrl,
- type = "webview",
- date = dateStr
- ))
-
- videoLinks.add(VideoLink(
- type = "play_page",
- url = fullUrl,
- title = linkText.ifBlank { finalNumber }
- ))
+ if (day != null && month != null && year != null) {
+ dateStr = if (year.length == 2) {
+ "20$year/$month/$day"
+ } else {
+ "$year/$month/$day"
+ }
+ episodeTitleStr = titlePart?.trim() ?: cleanLinkText
}
+ }
+
+ val episodeNumMatch = Pattern.compile("(\\d+)\\s*화|(\\d+)\\s*회|EP\\.?(\\d+)|제\\s*(\\d+)\\s*부").matcher(episodeTitleStr)
+ val episodeTitle = if (episodeNumMatch.find()) {
+ episodeNumMatch.group(1) ?: episodeNumMatch.group(2) ?: episodeNumMatch.group(3) ?: episodeNumMatch.group(4)
+ } else {
+ null
+ }
+
+ val finalNumber = if (!episodeTitle.isNullOrBlank()) {
+ episodeTitle
+ } else {
+ episodeIndex++.toString()
+ }
+
+ episodes.add(Episode(
+ number = dateStr.ifBlank { finalNumber },
+ title = episodeTitleStr.ifBlank { finalNumber },
+ url = fullUrl,
+ type = "webview",
+ date = dateStr
+ ))
+
+ videoLinks.add(VideoLink(
+ type = "play_page",
+ url = fullUrl,
+ title = linkText.ifBlank { finalNumber }
+ ))
+ }
+
+ // 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 ->
val numberStr = episode.number
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainActivity.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainActivity.kt
index 4fc3fb1..384feb9 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainActivity.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainActivity.kt
@@ -1,13 +1,126 @@
package com.example.tvmon.ui.main
+import android.app.Dialog
+import android.content.Context
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 com.example.tvmon.R
class MainActivity : FragmentActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ 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()
+ }
}
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt
index 409cb9a..aee7981 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt
@@ -57,9 +57,15 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
onItemViewClickedListener = this
onItemViewSelectedListener = this
- setOnSearchClickedListener {
- startActivity(Intent(requireContext(), com.example.tvmon.ui.search.SearchActivity::class.java))
+setOnSearchClickedListener {
+ 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")
}
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt
index c8ec8e8..b29a394 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/presenter/CardPresenters.kt
@@ -1,5 +1,6 @@
package com.example.tvmon.ui.presenter
+import android.view.View
import android.view.ViewGroup
import androidx.leanback.widget.ImageCardView
import androidx.leanback.widget.Presenter
@@ -10,44 +11,107 @@ import com.example.tvmon.data.model.Category
class ContentCardPresenter : 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))
- }
- return ViewHolder(cardView)
+ override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
+ val cardView = ImageCardView(parent.context).apply {
+ isFocusable = true
+ isFocusableInTouchMode = true
+ setBackgroundColor(parent.context.getColor(R.color.default_background))
+ }
+ 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 = null
+
+ 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 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
}
- 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 = null
-
- 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)
- }
+ 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()
+ }
}
- override fun onUnbindViewHolder(viewHolder: ViewHolder) {
- val cardView = viewHolder.view as ImageCardView
- cardView.badgeImage = null
- cardView.mainImage = null
+ 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() {
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchActivity.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchActivity.kt
index 8b9477e..a559532 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchActivity.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchActivity.kt
@@ -2,85 +2,102 @@ package com.example.tvmon.ui.search
import android.content.Intent
import android.os.Bundle
+import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.tvmon.R
import com.example.tvmon.data.model.Content
import com.example.tvmon.data.scraper.TvmonScraper
import com.example.tvmon.ui.detail.DetailsActivity
+import com.example.tvmon.ui.presenter.SearchCardPresenter
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class SearchActivity : AppCompatActivity() {
- companion object {
- private const val TAG = "TVMON_SEARCH"
+ companion object {
+ private const val TAG = "TVMON_SEARCH"
+ private const val NUM_COLUMNS = 4
+ }
+
+ private val scraper: TvmonScraper by inject()
+ private lateinit var searchView: SearchView
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var adapter: SearchResultsAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_search)
+
+ setupUI()
+
+ val initialQuery = intent.getStringExtra("initial_query")
+ if (!initialQuery.isNullOrBlank()) {
+ searchView.setQuery(initialQuery, true)
+ }
+ }
+
+ private fun setupUI() {
+ searchView = findViewById(R.id.search_view)
+ recyclerView = findViewById(R.id.search_results)
+
+ adapter = SearchResultsAdapter { content ->
+ openDetail(content)
}
- private val scraper: TvmonScraper by inject()
- private lateinit var searchView: SearchView
- private lateinit var recyclerView: RecyclerView
- private lateinit var adapter: SearchResultsAdapter
+ recyclerView.layoutManager = GridLayoutManager(this, NUM_COLUMNS)
+ recyclerView.adapter = adapter
+ recyclerView.setHasFixedSize(true)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_search)
-
- setupUI()
- }
-
- private fun setupUI() {
- searchView = findViewById(R.id.search_view)
- recyclerView = findViewById(R.id.search_results)
-
- adapter = SearchResultsAdapter { content ->
- openDetail(content)
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ query?.let {
+ if (it.isNotBlank()) {
+ search(it)
+ }
}
-
- recyclerView.layoutManager = LinearLayoutManager(this)
- recyclerView.adapter = adapter
+ return true
+ }
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String?): Boolean {
- query?.let {
- if (it.isNotBlank()) {
- search(it)
- }
- }
- return true
- }
-
- override fun onQueryTextChange(newText: String?): Boolean {
- newText?.let {
- if (it.length >= 2) {
- search(it)
- }
- }
- return true
- }
- })
-
- searchView.requestFocus()
- }
-
- private fun search(query: String) {
- lifecycleScope.launch {
- val result = scraper.search(query)
- runOnUiThread {
- if (result.success) {
- adapter.updateResults(result.results)
- }
- }
+ override fun onQueryTextChange(newText: String?): Boolean {
+ newText?.let {
+ if (it.length >= 2) {
+ search(it)
+ }
}
- }
+ return true
+ }
+ })
- private fun openDetail(content: Content) {
- val intent = Intent(this, DetailsActivity::class.java).apply {
- putExtra(DetailsActivity.EXTRA_CONTENT, content)
+ searchView.requestFocus()
+ }
+
+ private fun search(query: String) {
+ lifecycleScope.launch {
+ try {
+ val result = scraper.search(query)
+ runOnUiThread {
+ if (result.success && result.results.isNotEmpty()) {
+ adapter.updateResults(result.results)
+ } else {
+ adapter.updateResults(emptyList())
+ }
}
- startActivity(intent)
+ } catch (e: Exception) {
+ runOnUiThread {
+ adapter.updateResults(emptyList())
+ }
+ }
}
+ }
+
+ private fun openDetail(content: Content) {
+ val intent = Intent(this, DetailsActivity::class.java).apply {
+ putExtra(DetailsActivity.EXTRA_CONTENT, content)
+ }
+ startActivity(intent)
+ }
}
diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchResultsAdapter.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchResultsAdapter.kt
index f62d740..5c10722 100644
--- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchResultsAdapter.kt
+++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/search/SearchResultsAdapter.kt
@@ -1,61 +1,37 @@
package com.example.tvmon.ui.search
-import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
+import androidx.leanback.widget.Presenter
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.ui.presenter.SearchCardPresenter
class SearchResultsAdapter(
- private val onItemClick: (Content) -> Unit
+ private val onItemClick: (Content) -> Unit
) : RecyclerView.Adapter() {
-
- private var results = mutableListOf()
-
- fun updateResults(newResults: List) {
- results.clear()
- results.addAll(newResults)
- notifyDataSetChanged()
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.item_search_result, parent, false)
- return ViewHolder(view)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val content = results[position]
- holder.bind(content, onItemClick)
- }
-
- override fun getItemCount(): Int = results.size
-
- class ViewHolder(view: View) : RecyclerView.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) }
- }
- }
+
+ private val presenter = SearchCardPresenter()
+ private var results = mutableListOf()
+
+ fun updateResults(newResults: List) {
+ results.clear()
+ results.addAll(newResults)
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(presenter.onCreateViewHolder(parent))
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val content = results[position]
+ presenter.onBindViewHolder(holder.viewHolder, content)
+ holder.viewHolder.view.setOnClickListener { onItemClick(content) }
+ }
+
+
+
+ override fun getItemCount(): Int = results.size
+
+ class ViewHolder(val viewHolder: Presenter.ViewHolder) : RecyclerView.ViewHolder(viewHolder.view)
}