Fix episode number matching, add scroll-based preloading, and fix search UI
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 }
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
10
version.json
10
version.json
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user