Fix episode number matching, add scroll-based preloading, and fix search UI

This commit is contained in:
tvmon-dev
2026-04-15 20:45:28 +09:00
parent 9a7ea53c1e
commit dae2fa6082
7 changed files with 75 additions and 36 deletions

View File

@@ -58,7 +58,8 @@
android:name=".ui.search.SearchActivity" android:name=".ui.search.SearchActivity"
android:exported="true" android:exported="true"
android:label="@string/search_title" android:label="@string/search_title"
android:theme="@style/Theme.Tvmon.Search"> android:screenOrientation="landscape"
android:theme="@style/Theme.Leanback">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
</intent-filter> </intent-filter>

View File

@@ -410,6 +410,7 @@ 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 seenNumbers = mutableSetOf<String>()
val seriesId = seriesUrl.substringAfterLast("/").substringBefore("?") val seriesId = seriesUrl.substringAfterLast("/").substringBefore("?")
@@ -437,14 +438,18 @@ val episodes = mutableListOf<Episode>()
val episodeNumMatch = Pattern.compile("(\\d+)\\s*화|(\\d+)\\s*회|EP\\.?(\\d+)|제\\s*(\\d+)\\s*부").matcher(linkText) val episodeNumMatch = Pattern.compile("(\\d+)\\s*화|(\\d+)\\s*회|EP\\.?(\\d+)|제\\s*(\\d+)\\s*부").matcher(linkText)
val episodeTitle = if (episodeNumMatch.find()) { val episodeTitle = if (episodeNumMatch.find()) {
episodeNumMatch.group(1) ?: episodeNumMatch.group(2) ?: episodeNumMatch.group(3) ?: episodeNumMatch.group(4) ?: episodeId episodeNumMatch.group(1) ?: episodeNumMatch.group(2) ?: episodeNumMatch.group(3) ?: episodeNumMatch.group(4)
} else { } else {
episodeId null
} }
val finalNumber = episodeTitle ?: episodeId
if (finalNumber in seenNumbers) continue
seenNumbers.add(finalNumber)
episodes.add(Episode( episodes.add(Episode(
number = episodeTitle, number = finalNumber,
title = linkText.ifBlank { episodeTitle }, title = linkText.ifBlank { finalNumber },
url = fullUrl, url = fullUrl,
type = "webview" type = "webview"
)) ))
@@ -452,7 +457,7 @@ val episodes = mutableListOf<Episode>()
videoLinks.add(VideoLink( videoLinks.add(VideoLink(
type = "play_page", type = "play_page",
url = fullUrl, url = fullUrl,
title = linkText.ifBlank { episodeTitle } title = linkText.ifBlank { finalNumber }
)) ))
} }

View File

@@ -24,7 +24,7 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
companion object { companion object {
private const val TAG = "TVMON_MAIN" private const val TAG = "TVMON_MAIN"
private const val PRELOAD_THRESHOLD = 5 // 로딩 시작 위치 private const val PRELOAD_THRESHOLD = 20
} }
private val scraper: TvmonScraper by inject() private val scraper: TvmonScraper by inject()
@@ -38,6 +38,8 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private var currentSelectedRowIndex = -1 private var currentSelectedRowIndex = -1
private var isDataLoaded = false private var isDataLoaded = false
private var lastPreloadedPage = mutableMapOf<String, Int>()
private var currentCategoryKey: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -312,8 +314,24 @@ private fun loadNextPage(categoryKey: String, page: Int) {
if (row is ListRow) { if (row is ListRow) {
val rowIndex = rowsAdapter.indexOf(row) val rowIndex = rowsAdapter.indexOf(row)
if (rowIndex >= 0) { if (rowIndex >= 0) {
val categoryKey = findCategoryKeyForRow(rowIndex)
currentCategoryKey = categoryKey
handleRowSelection(rowIndex) handleRowSelection(rowIndex)
} }
} }
} }
private fun checkAndPreload(categoryKey: String, position: Int) {
if (position >= PRELOAD_THRESHOLD) {
val currentPage = categoryPages[categoryKey] ?: 1
val maxPage = categoryMaxPage[categoryKey] ?: 1
val lastPreload = lastPreloadedPage[categoryKey] ?: 0
if (currentPage < maxPage && currentPage > lastPreload) {
Log.w(TAG, "checkAndPreload: $categoryKey at position $position, preloading page ${currentPage + 1}")
lastPreloadedPage[categoryKey] = currentPage
preloadNextPage(categoryKey, currentPage + 1)
}
}
}
} }

View File

@@ -1,6 +1,5 @@
package com.example.tvmon.ui.search package com.example.tvmon.ui.search
import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -16,26 +15,23 @@ import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
class SearchActivity : AppCompatActivity() { class SearchActivity : AppCompatActivity() {
companion object {
private const val TAG = "TVMON_SEARCH"
}
private val scraper: TvmonScraper by inject() private val scraper: TvmonScraper by inject()
private lateinit var searchView: SearchView
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var adapter: SearchResultsAdapter private lateinit var adapter: SearchResultsAdapter
private lateinit var searchView: SearchView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) setContentView(R.layout.activity_search)
setupUI() setupUI()
if (Intent.ACTION_SEARCH == intent.action) {
val query = intent.getStringExtra(SearchManager.QUERY)
if (!query.isNullOrBlank()) {
search(query)
}
}
} }
private fun setupUI() { private fun setupUI() {
searchView = findViewById(R.id.search_view) searchView = findViewById(R.id.search_view)
recyclerView = findViewById(R.id.search_results) recyclerView = findViewById(R.id.search_results)
@@ -43,34 +39,44 @@ class SearchActivity : AppCompatActivity() {
adapter = SearchResultsAdapter { content -> adapter = SearchResultsAdapter { content ->
openDetail(content) openDetail(content)
} }
recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter recyclerView.adapter = adapter
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
if (!query.isNullOrBlank()) { query?.let {
search(query) if (it.isNotBlank()) {
search(it)
}
} }
return true return true
} }
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
return false newText?.let {
if (it.length >= 2) {
search(it)
}
}
return true
} }
}) })
searchView.requestFocus() searchView.requestFocus()
} }
private fun search(query: String) { private fun search(query: String) {
lifecycleScope.launch { lifecycleScope.launch {
val result = scraper.search(query) val result = scraper.search(query)
if (result.success) { runOnUiThread {
adapter.updateResults(result.results) if (result.success) {
adapter.updateResults(result.results)
}
} }
} }
} }
private fun openDetail(content: Content) { private fun openDetail(content: Content) {
val intent = Intent(this, DetailsActivity::class.java).apply { val intent = Intent(this, DetailsActivity::class.java).apply {
putExtra(DetailsActivity.EXTRA_CONTENT, content) putExtra(DetailsActivity.EXTRA_CONTENT, content)

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#333333" />
<corners android:radius="8dp" />
<padding android:left="16dp" android:right="16dp" android:top="12dp" android:bottom="12dp" />
</shape>

View File

@@ -3,18 +3,20 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:background="@color/default_background"
android:padding="32dp">
<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:id="@+id/search_view" android:id="@+id/search_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:iconifiedByDefault="false" android:iconifiedByDefault="false"
android:queryHint="@string/search_hint" /> android:queryHint="@string/search_hint" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_results" android:id="@+id/search_results"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="16dp" /> android:layout_marginTop="24dp" />
</LinearLayout> </LinearLayout>

View File

@@ -1,6 +1,6 @@
{ {
"versionCode": 1, "versionCode": 2,
"versionName": "1.0.0", "versionName": "1.0.1",
"apkUrl": "https://git.webpluss.net/sanjeok77/NeFLIX_release/releases/download/v1.0.0/app-release.apk", "apkUrl": "https://git.webpluss.net/sanjeok77/tvmon_release/releases/download/v1.0.1/app-release.apk",
"updateMessage": "tvmon v1.0.0 - Android TV app with pagination fix and cast display improvements" "updateMessage": "v1.0.1 - Bug fixes"
} }