diff --git a/.idea/misc.xml b/.idea/misc.xml
index 7f598c7c9..4fd703491 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -9,6 +9,11 @@
+
+
+
+
+
@@ -23,6 +28,7 @@
+
@@ -31,7 +37,11 @@
+
+
+
+
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt
index 51b7571db..ee8255dfc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt
@@ -13,6 +13,14 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE manga_id = :id")
abstract suspend fun find(id: Long): MangaWithTags?
+ @Transaction
+ @Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit")
+ abstract suspend fun searchByTitle(query: String, limit: Int): List
+
+ @Transaction
+ @Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit")
+ abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List
+
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(manga: MangaEntity): Long
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
index 443ab118f..cd95e7e8a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
@@ -11,9 +11,12 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.ActionBarDrawerToggle
+import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.Insets
import androidx.core.view.*
import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentTransaction
+import androidx.fragment.app.commit
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
@@ -24,34 +27,41 @@ import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSection
import org.koitharu.kotatsu.databinding.ActivityMainBinding
+import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.favourites.ui.FavouritesContainerFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.local.ui.LocalListFragment
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
-import org.koitharu.kotatsu.search.ui.SearchHelper
+import org.koitharu.kotatsu.search.ui.SearchActivity
+import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
+import org.koitharu.kotatsu.search.ui.suggestion.SearchUI
import org.koitharu.kotatsu.settings.AppUpdateChecker
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.resolveDp
-import java.io.Closeable
class MainActivity : BaseActivity(),
NavigationView.OnNavigationItemSelectedListener,
- View.OnClickListener {
+ View.OnClickListener, SearchSuggestionListener, MenuItem.OnActionExpandListener {
private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE)
+ private val searchSuggestionViewModel by viewModel(
+ mode = LazyThreadSafetyMode.NONE
+ )
private lateinit var drawerToggle: ActionBarDrawerToggle
- private var closeable: Closeable? = null
+ private var searchUi: SearchUI? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater))
- drawerToggle =
- ActionBarDrawerToggle(
+ drawerToggle = ActionBarDrawerToggle(
this,
binding.drawer,
binding.toolbar,
@@ -68,7 +78,7 @@ class MainActivity : BaseActivity(),
setOnClickListener(this@MainActivity)
}
- supportFragmentManager.findFragmentById(R.id.container)?.let {
+ supportFragmentManager.findFragmentByTag(TAG_PRIMARY)?.let {
binding.fab.isVisible = it is HistoryListFragment
} ?: run {
openDefaultSection()
@@ -84,11 +94,6 @@ class MainActivity : BaseActivity(),
viewModel.remoteSources.observe(this, this::updateSideMenu)
}
- override fun onDestroy() {
- closeable?.close()
- super.onDestroy()
- }
-
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
drawerToggle.syncState()
@@ -109,8 +114,10 @@ class MainActivity : BaseActivity(),
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_main, menu)
- menu.findItem(R.id.action_search)?.let { menuItem ->
- closeable = SearchHelper.setupSearchView(menuItem)
+ searchUi = menu.findItem(R.id.action_search)?.let { menuItem ->
+ onMenuItemActionCollapse(menuItem)
+ menuItem.setOnActionExpandListener(this)
+ SearchUI.from(menuItem, this)
}
return super.onCreateOptionsMenu(menu)
}
@@ -131,28 +138,32 @@ class MainActivity : BaseActivity(),
if (item.groupId == R.id.group_remote_sources) {
val source = MangaSource.values().getOrNull(item.itemId) ?: return false
setPrimaryFragment(RemoteListFragment.newInstance(source))
- } else when (item.itemId) {
- R.id.nav_history -> {
- viewModel.defaultSection = AppSection.HISTORY
- setPrimaryFragment(HistoryListFragment.newInstance())
+ searchSuggestionViewModel.onSourceChanged(source)
+ } else {
+ searchSuggestionViewModel.onSourceChanged(null)
+ when (item.itemId) {
+ R.id.nav_history -> {
+ viewModel.defaultSection = AppSection.HISTORY
+ setPrimaryFragment(HistoryListFragment.newInstance())
+ }
+ R.id.nav_favourites -> {
+ viewModel.defaultSection = AppSection.FAVOURITES
+ setPrimaryFragment(FavouritesContainerFragment.newInstance())
+ }
+ R.id.nav_local_storage -> {
+ viewModel.defaultSection = AppSection.LOCAL
+ setPrimaryFragment(LocalListFragment.newInstance())
+ }
+ R.id.nav_feed -> {
+ viewModel.defaultSection = AppSection.FEED
+ setPrimaryFragment(FeedFragment.newInstance())
+ }
+ R.id.nav_action_settings -> {
+ startActivity(SettingsActivity.newIntent(this))
+ return true
+ }
+ else -> return false
}
- R.id.nav_favourites -> {
- viewModel.defaultSection = AppSection.FAVOURITES
- setPrimaryFragment(FavouritesContainerFragment.newInstance())
- }
- R.id.nav_local_storage -> {
- viewModel.defaultSection = AppSection.LOCAL
- setPrimaryFragment(LocalListFragment.newInstance())
- }
- R.id.nav_feed -> {
- viewModel.defaultSection = AppSection.FEED
- setPrimaryFragment(FeedFragment.newInstance())
- }
- R.id.nav_action_settings -> {
- startActivity(SettingsActivity.newIntent(this))
- return true
- }
- else -> return false
}
binding.drawer.closeDrawers()
return true
@@ -171,6 +182,62 @@ class MainActivity : BaseActivity(),
}
}
+ override fun onMangaClick(manga: Manga) {
+ startActivity(DetailsActivity.newIntent(this, manga))
+ }
+
+ override fun onQueryClick(query: String, submit: Boolean) {
+ if (submit) {
+ if (query.isNotEmpty()) {
+ val source = searchSuggestionViewModel.getLocalSearchSource()
+ if (source != null) {
+ startActivity(SearchActivity.newIntent(this, source, query))
+ } else {
+ startActivity(GlobalSearchActivity.newIntent(this, query))
+ }
+ searchSuggestionViewModel.saveQuery(query)
+ }
+ } else {
+ searchUi?.query = query
+ }
+ }
+
+ override fun onQueryChanged(query: String) {
+ searchSuggestionViewModel.onQueryChanged(query)
+ }
+
+ override fun onClearSearchHistory() {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.clear_search_history)
+ .setMessage(R.string.text_clear_search_history_prompt)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.clear) { _, _ ->
+ searchSuggestionViewModel.clearSearchHistory()
+ }.show()
+ }
+
+ override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH)
+ if (fragment == null) {
+ supportFragmentManager.commit {
+ add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH)
+ setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ }
+ }
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
+ val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH)
+ if (fragment != null) {
+ supportFragmentManager.commit {
+ remove(fragment)
+ setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ }
+ }
+ return true
+ }
+
private fun onOpenReader(manga: Manga) {
val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ActivityOptions.makeClipRevealAnimation(
@@ -234,8 +301,14 @@ class MainActivity : BaseActivity(),
private fun setPrimaryFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
- .replace(R.id.container, fragment)
+ .replace(R.id.container, fragment, TAG_PRIMARY)
.commit()
binding.fab.isVisible = fragment is HistoryListFragment
}
+
+ private companion object {
+
+ const val TAG_PRIMARY = "primary"
+ const val TAG_SEARCH = "search"
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
index a6a7db5c5..da6239420 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
@@ -1,17 +1,22 @@
package org.koitharu.kotatsu.search
+import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
+import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.search.ui.SearchViewModel
import org.koitharu.kotatsu.search.ui.global.GlobalSearchViewModel
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
val searchModule
get() = module {
- single { MangaSearchRepository(get()) }
+ single { MangaSearchRepository(get(), get(), androidContext(), get()) }
+
+ factory { MangaSuggestionsProvider.createSuggestions(androidContext()) }
viewModel { (source: MangaSource, query: String) ->
SearchViewModel(get(named(source)), query, get())
@@ -19,4 +24,5 @@ val searchModule
viewModel { (query: String) ->
GlobalSearchViewModel(query, get(), get())
}
+ viewModel { SearchSuggestionViewModel(get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt
index 1efec6723..63e774884 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt
@@ -1,14 +1,29 @@
package org.koitharu.kotatsu.search.domain
import android.annotation.SuppressLint
+import android.app.SearchManager
+import android.content.Context
+import android.provider.SearchRecentSuggestions
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
+import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.Manga
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.ext.levenshteinDistance
-class MangaSearchRepository(private val settings: AppSettings) {
+class MangaSearchRepository(
+ private val settings: AppSettings,
+ private val db: MangaDatabase,
+ private val context: Context,
+ private val recentSuggestions: SearchRecentSuggestions,
+) {
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow =
MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
@@ -22,16 +37,81 @@ class MangaSearchRepository(private val settings: AppSettings) {
match(it, query)
}
+ suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List {
+ if (query.isEmpty()) {
+ return emptyList()
+ }
+ return if (source != null) {
+ db.mangaDao.searchByTitle("%$query%", source.name, limit)
+ } else {
+ db.mangaDao.searchByTitle("%$query%", limit)
+ }.map { it.toManga() }
+ .sortedBy { x -> x.title.levenshteinDistance(query) }
+ }
+
+ suspend fun getQuerySuggestion(
+ query: String,
+ limit: Int,
+ ): List = withContext(Dispatchers.IO) {
+ context.contentResolver.query(
+ MangaSuggestionsProvider.QUERY_URI,
+ SUGGESTION_PROJECTION,
+ "${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?",
+ arrayOf("%$query%"),
+ "date DESC"
+ )?.use { cursor ->
+ val count = minOf(cursor.count, limit)
+ if (count == 0) {
+ return@withContext emptyList()
+ }
+ val result = ArrayList(count)
+ if (cursor.moveToFirst()) {
+ val index = cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY)
+ do {
+ result += cursor.getString(index)
+ } while (currentCoroutineContext().isActive && cursor.moveToNext())
+ }
+ result
+ }.orEmpty()
+ }
+
+ fun saveSearchQuery(query: String) {
+ recentSuggestions.saveRecentQuery(query, null)
+ }
+
+ suspend fun clearSearchHistory(): Unit = withContext(Dispatchers.IO) {
+ recentSuggestions.clearHistory()
+ }
+
+ suspend fun deleteSearchQuery(query: String) = withContext(Dispatchers.IO) {
+ context.contentResolver.delete(
+ MangaSuggestionsProvider.URI,
+ "display1 = ?",
+ arrayOf(query),
+ )
+ }
+
+ suspend fun getSearchHistoryCount(): Int = withContext(Dispatchers.IO) {
+ context.contentResolver.query(
+ MangaSuggestionsProvider.QUERY_URI,
+ SUGGESTION_PROJECTION,
+ null,
+ null,
+ null
+ )?.use { cursor -> cursor.count } ?: 0
+ }
+
private companion object {
private val REGEX_SPACE = Regex("\\s+")
+ val SUGGESTION_PROJECTION = arrayOf(SearchManager.SUGGEST_COLUMN_QUERY)
@SuppressLint("DefaultLocale")
fun match(manga: Manga, query: String): Boolean {
val words = HashSet()
- words += manga.title.toLowerCase().split(REGEX_SPACE)
- words += manga.altTitle?.toLowerCase()?.split(REGEX_SPACE).orEmpty()
- val words2 = query.toLowerCase().split(REGEX_SPACE).toSet()
+ words += manga.title.lowercase().split(REGEX_SPACE)
+ words += manga.altTitle?.lowercase()?.split(REGEX_SPACE).orEmpty()
+ val words2 = query.lowercase().split(REGEX_SPACE).toSet()
for (w in words) {
for (w2 in words2) {
val diff = w.levenshteinDistance(w2) / ((w.length + w2.length) / 2f)
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt
index 2889a24cf..c708db286 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt
@@ -4,49 +4,14 @@ import android.app.SearchManager
import android.content.ContentResolver
import android.content.Context
import android.content.SearchRecentSuggestionsProvider
-import android.database.Cursor
import android.net.Uri
import android.provider.SearchRecentSuggestions
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.cursoradapter.widget.CursorAdapter
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig
-import org.koitharu.kotatsu.R
class MangaSuggestionsProvider : SearchRecentSuggestionsProvider() {
init {
- setupSuggestions(
- AUTHORITY,
- MODE
- )
- }
-
- private class SearchSuggestionAdapter(context: Context, cursor: Cursor) : CursorAdapter(
- context, cursor,
- FLAG_REGISTER_CONTENT_OBSERVER
- ) {
-
- override fun newView(context: Context, cursor: Cursor?, parent: ViewGroup?): View {
- return LayoutInflater.from(context)
- .inflate(R.layout.item_search_complete, parent, false)
- }
-
- override fun bindView(view: View, context: Context, cursor: Cursor) {
- if (view !is TextView) return
- view.text = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
- }
-
- override fun convertToString(cursor: Cursor?): CharSequence {
- return cursor?.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
- .orEmpty()
- }
+ setupSuggestions(AUTHORITY, MODE)
}
companion object {
@@ -54,65 +19,16 @@ class MangaSuggestionsProvider : SearchRecentSuggestionsProvider() {
private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.MangaSuggestionsProvider"
private const val MODE = DATABASE_MODE_QUERIES
- private val uri = Uri.Builder()
+ fun createSuggestions(context: Context): SearchRecentSuggestions {
+ return SearchRecentSuggestions(context, AUTHORITY, MODE)
+ }
+
+ val QUERY_URI: Uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY)
.build()
- private val projection = arrayOf("_id", SearchManager.SUGGEST_COLUMN_QUERY)
-
- fun saveQueryAsync(context: Context, query: String) {
- GlobalScope.launch(Dispatchers.IO) {
- saveQuery(context, query)
- }
- }
-
- fun saveQuery(context: Context, query: String) {
- runCatching {
- SearchRecentSuggestions(
- context,
- AUTHORITY,
- MODE
- ).saveRecentQuery(query, null)
- }.onFailure {
- if (BuildConfig.DEBUG) {
- it.printStackTrace()
- }
- }
- }
-
- suspend fun clearHistory(context: Context) = withContext(Dispatchers.IO) {
- SearchRecentSuggestions(
- context,
- AUTHORITY,
- MODE
- ).clearHistory()
- }
-
- suspend fun getItemsCount(context: Context) = withContext(Dispatchers.IO) {
- getCursor(context)?.use { it.count } ?: 0
- }
-
- private fun getCursor(context: Context): Cursor? {
- return context.contentResolver?.query(uri, projection, null, arrayOf(""), null)
- }
-
- @Deprecated("Need async implementation")
- fun getSuggestionAdapter(context: Context): CursorAdapter? = getCursor(
- context
- )?.let { cursor ->
- SearchSuggestionAdapter(context, cursor).also {
- it.setFilterQueryProvider { q ->
- context.contentResolver?.query(
- uri,
- projection,
- " ?",
- arrayOf(q?.toString().orEmpty()),
- null
- )
- }
- }
- }
+ val URI: Uri = Uri.parse("content://$AUTHORITY/suggestions")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
index 05092438f..7221f6d11 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
@@ -7,14 +7,20 @@ import android.os.Parcelable
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
+import androidx.fragment.app.commit
+import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.utils.ext.showKeyboard
class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
+ private val searchSuggestionViewModel by viewModel(
+ mode = LazyThreadSafetyMode.NONE
+ )
private lateinit var source: MangaSource
override fun onCreate(savedInstanceState: Bundle?) {
@@ -28,8 +34,6 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
supportActionBar?.setDisplayHomeAsUpEnabled(true)
with(binding.searchView) {
queryHint = getString(R.string.search_on_s, source.title)
- suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(this@SearchActivity)
- setOnSuggestionListener(SearchHelper.SuggestionListener(this))
setOnQueryTextListener(this@SearchActivity)
if (query.isNullOrBlank()) {
@@ -41,11 +45,6 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
}
}
- override fun onDestroy() {
- binding.searchView.suggestionsAdapter.changeCursor(null) //close cursor
- super.onDestroy()
- }
-
override fun onWindowInsetsChanged(insets: Insets) {
binding.toolbar.updatePadding(
top = insets.top,
@@ -55,19 +54,20 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
}
override fun onQueryTextSubmit(query: String?): Boolean {
- return if (!query.isNullOrBlank()) {
- title = query
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.container, SearchFragment.newInstance(source, query))
- .commit()
- binding.searchView.clearFocus()
- MangaSuggestionsProvider.saveQueryAsync(applicationContext, query)
- true
- } else false
+ val q = query?.trim()
+ if (q.isNullOrEmpty()) {
+ return false
+ }
+ title = query
+ supportFragmentManager.commit {
+ replace(R.id.container, SearchFragment.newInstance(source, q))
+ }
+ binding.searchView.clearFocus()
+ searchSuggestionViewModel.saveQuery(q)
+ return true
}
- override fun onQueryTextChange(newText: String?) = false
+ override fun onQueryTextChange(newText: String?): Boolean = false
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchHelper.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchHelper.kt
deleted file mode 100644
index 6f937b2eb..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchHelper.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.koitharu.kotatsu.search.ui
-
-import android.app.SearchManager
-import android.content.Context
-import android.database.Cursor
-import android.view.MenuItem
-import androidx.appcompat.widget.SearchView
-import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
-import java.io.Closeable
-
-object SearchHelper {
-
- fun setupSearchView(menuItem: MenuItem): Closeable? {
- val view = menuItem.actionView as? SearchView ?: return null
- val context = view.context
- val adapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
- view.queryHint = context.getString(R.string.search_manga)
- view.suggestionsAdapter = adapter
- view.setOnQueryTextListener(QueryListener(context))
- view.setOnSuggestionListener(SuggestionListener(view))
- return adapter?.cursor
- }
-
- private class QueryListener(private val context: Context) :
- SearchView.OnQueryTextListener {
-
- override fun onQueryTextSubmit(query: String?): Boolean {
- return if (!query.isNullOrBlank()) {
- context.startActivity(GlobalSearchActivity.newIntent(context, query.trim()))
- MangaSuggestionsProvider.saveQueryAsync(context.applicationContext, query)
- true
- } else false
- }
-
- override fun onQueryTextChange(newText: String?) = false
- }
-
- class SuggestionListener(private val view: SearchView) :
- SearchView.OnSuggestionListener {
-
- override fun onSuggestionSelect(position: Int) = false
-
- override fun onSuggestionClick(position: Int): Boolean {
- val query = runCatching {
- val c = view.suggestionsAdapter.getItem(position) as? Cursor
- c?.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
- }.getOrNull() ?: return false
- view.setQuery(query, true)
- return true
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt
new file mode 100644
index 000000000..e053df0fc
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt
@@ -0,0 +1,57 @@
+package org.koitharu.kotatsu.search.ui.suggestion
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.graphics.Insets
+import androidx.core.view.updatePadding
+import androidx.recyclerview.widget.ItemTouchHelper
+import org.koin.android.ext.android.get
+import org.koin.androidx.viewmodel.ext.android.sharedViewModel
+import org.koitharu.kotatsu.base.ui.BaseFragment
+import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding
+import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter
+
+class SearchSuggestionFragment : BaseFragment(),
+ SearchSuggestionItemCallback.SuggestionItemListener {
+
+ private val viewModel by sharedViewModel()
+
+ override fun onInflateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ ) = FragmentSearchSuggestionBinding.inflate(inflater, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val adapter = SearchSuggestionAdapter(
+ coil = get(),
+ lifecycleOwner = viewLifecycleOwner,
+ listener = requireActivity() as SearchSuggestionListener,
+ )
+ binding.root.adapter = adapter
+ viewModel.suggestion.observe(viewLifecycleOwner) {
+ adapter.items = it
+ }
+ ItemTouchHelper(SearchSuggestionItemCallback(this))
+ .attachToRecyclerView(binding.root)
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ binding.root.updatePadding(
+ left = insets.left,
+ right = insets.right,
+ bottom = insets.bottom,
+ )
+ }
+
+ override fun onRemoveQuery(query: String) {
+ viewModel.deleteQuery(query)
+ }
+
+ companion object {
+
+ fun newInstance() = SearchSuggestionFragment()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt
new file mode 100644
index 000000000..e983be92b
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt
@@ -0,0 +1,42 @@
+package org.koitharu.kotatsu.search.ui.suggestion
+
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter
+import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
+import org.koitharu.kotatsu.utils.ext.getItem
+
+class SearchSuggestionItemCallback(
+ private val listener: SuggestionItemListener,
+) : ItemTouchHelper.Callback() {
+
+ private val movementFlags = makeMovementFlags(
+ 0,
+ ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
+ )
+
+ override fun getMovementFlags(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ ): Int = if (viewHolder.itemViewType == SearchSuggestionAdapter.ITEM_TYPE_QUERY) {
+ movementFlags
+ } else {
+ 0
+ }
+
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder,
+ ): Boolean = false
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ val item = viewHolder.getItem() ?: return
+ listener.onRemoveQuery(item.query)
+ }
+
+ interface SuggestionItemListener {
+
+ fun onRemoveQuery(query: String)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt
new file mode 100644
index 000000000..d82a47bb0
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt
@@ -0,0 +1,14 @@
+package org.koitharu.kotatsu.search.ui.suggestion
+
+import org.koitharu.kotatsu.core.model.Manga
+
+interface SearchSuggestionListener {
+
+ fun onMangaClick(manga: Manga)
+
+ fun onQueryClick(query: String, submit: Boolean)
+
+ fun onQueryChanged(query: String)
+
+ fun onClearSearchHistory()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
new file mode 100644
index 000000000..d05382e72
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
@@ -0,0 +1,95 @@
+package org.koitharu.kotatsu.search.ui.suggestion
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.plus
+import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.core.model.MangaSource
+import org.koitharu.kotatsu.search.domain.MangaSearchRepository
+import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
+
+class SearchSuggestionViewModel(
+ private val repository: MangaSearchRepository,
+) : BaseViewModel() {
+
+ private val query = MutableStateFlow("")
+ private val source = MutableStateFlow(null)
+ private val isLocalSearch = MutableStateFlow(false)
+ private var suggestionJob: Job? = null
+
+ val suggestion = MutableLiveData>()
+
+ init {
+ setupSuggestion()
+ }
+
+ fun onQueryChanged(newQuery: String) {
+ query.value = newQuery
+ }
+
+ fun onSourceChanged(newSource: MangaSource?) {
+ source.value = newSource
+ }
+
+ fun saveQuery(query: String) {
+ repository.saveSearchQuery(query)
+ }
+
+ fun getLocalSearchSource(): MangaSource? {
+ return source.value?.takeIf { isLocalSearch.value }
+ }
+
+ fun clearSearchHistory() {
+ launchJob {
+ repository.clearSearchHistory()
+ setupSuggestion()
+ }
+ }
+
+ fun deleteQuery(query: String) {
+ launchJob {
+ repository.deleteSearchQuery(query)
+ setupSuggestion()
+ }
+ }
+
+ private fun setupSuggestion() {
+ suggestionJob?.cancel()
+ suggestionJob = combine(
+ query
+ .debounce(DEBOUNCE_TIMEOUT)
+ .mapLatest { q ->
+ q to repository.getQuerySuggestion(q, MAX_QUERY_ITEMS)
+ },
+ source,
+ isLocalSearch
+ ) { (q, queries), src, srcOnly ->
+ val result = ArrayList(MAX_SUGGESTION_ITEMS)
+ if (src != null) {
+ result += SearchSuggestionItem.Header(src, isLocalSearch)
+ }
+ if (q.length >= SEARCH_THRESHOLD) {
+ repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
+ .mapTo(result) {
+ SearchSuggestionItem.MangaItem(it)
+ }
+ }
+ queries.mapTo(result) { SearchSuggestionItem.RecentQuery(it) }
+ result
+ }.onEach {
+ suggestion.postValue(it)
+ }.launchIn(viewModelScope + Dispatchers.Default)
+ }
+
+ private companion object {
+
+ const val DEBOUNCE_TIMEOUT = 500L
+ const val SEARCH_THRESHOLD = 3
+ const val MAX_MANGA_ITEMS = 3
+ const val MAX_QUERY_ITEMS = 120
+ const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 1
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt
new file mode 100644
index 000000000..fb9f79847
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt
@@ -0,0 +1,51 @@
+package org.koitharu.kotatsu.search.ui.suggestion
+
+import android.view.MenuItem
+import androidx.appcompat.widget.SearchView
+import org.koitharu.kotatsu.R
+
+class SearchUI(
+ private val searchView: SearchView,
+ listener: SearchSuggestionListener,
+ hint: String? = null,
+) {
+
+ init {
+ val context = searchView.context
+ searchView.queryHint = hint ?: context.getString(R.string.search_manga)
+ searchView.setOnQueryTextListener(QueryListener(listener))
+ }
+
+ var query: String
+ get() = searchView.query.toString()
+ set(value) {
+ searchView.setQuery(value, false)
+ }
+
+ private class QueryListener(
+ private val listener: SearchSuggestionListener,
+ ) : SearchView.OnQueryTextListener {
+
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return if (!query.isNullOrBlank()) {
+ listener.onQueryClick(query.trim(), submit = true)
+ true
+ } else false
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ listener.onQueryChanged(newText?.trim().orEmpty())
+ return true
+ }
+ }
+
+ companion object {
+
+ fun from(
+ menuItem: MenuItem,
+ listener: SearchSuggestionListener,
+ ): SearchUI? = (menuItem.actionView as? SearchView)?.let {
+ SearchUI(it, listener)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt
new file mode 100644
index 000000000..06e6af858
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt
@@ -0,0 +1,51 @@
+package org.koitharu.kotatsu.search.ui.suggestion.adapter
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.recyclerview.widget.DiffUtil
+import coil.ImageLoader
+import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
+import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
+import kotlin.jvm.internal.Intrinsics
+
+class SearchSuggestionAdapter(
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
+ listener: SearchSuggestionListener,
+) : AsyncListDifferDelegationAdapter(DiffCallback()) {
+
+ init {
+ delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener))
+ .addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
+ .addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener))
+ }
+
+ private class DiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(
+ oldItem: SearchSuggestionItem,
+ newItem: SearchSuggestionItem,
+ ): Boolean = when {
+ oldItem is SearchSuggestionItem.MangaItem && newItem is SearchSuggestionItem.MangaItem -> {
+ oldItem.manga.id == newItem.manga.id
+ }
+ oldItem is SearchSuggestionItem.RecentQuery && newItem is SearchSuggestionItem.RecentQuery -> {
+ oldItem.query == newItem.query
+ }
+ oldItem is SearchSuggestionItem.Header && newItem is SearchSuggestionItem.Header -> true
+ else -> false
+ }
+
+ override fun areContentsTheSame(
+ oldItem: SearchSuggestionItem,
+ newItem: SearchSuggestionItem,
+ ): Boolean = Intrinsics.areEqual(oldItem, newItem)
+ }
+
+ companion object {
+
+ const val ITEM_TYPE_MANGA = 0
+ const val ITEM_TYPE_QUERY = 1
+ const val ITEM_TYPE_HEADER = 2
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionHeaderAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionHeaderAD.kt
new file mode 100644
index 000000000..be60708cd
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionHeaderAD.kt
@@ -0,0 +1,29 @@
+package org.koitharu.kotatsu.search.ui.suggestion.adapter
+
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.databinding.ItemSearchSuggestionHeaderBinding
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
+import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
+
+fun searchSuggestionHeaderAD(
+ listener: SearchSuggestionListener,
+) = adapterDelegateViewBinding(
+ { inflater, parent -> ItemSearchSuggestionHeaderBinding.inflate(inflater, parent, false) }
+ ) {
+
+ binding.switchLocal.setOnCheckedChangeListener { _, isChecked ->
+ item.isChecked.value = isChecked
+ }
+ binding.buttonClear.setOnClickListener {
+ listener.onClearSearchHistory()
+ }
+
+ bind {
+ binding.switchLocal.text = getString(
+ R.string.search_only_on_s,
+ item.source.title,
+ )
+ binding.switchLocal.isChecked = item.isChecked.value
+ }
+ }
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionMangaAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionMangaAD.kt
new file mode 100644
index 000000000..2eed6b932
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionMangaAD.kt
@@ -0,0 +1,46 @@
+package org.koitharu.kotatsu.search.ui.suggestion.adapter
+
+import androidx.lifecycle.LifecycleOwner
+import coil.ImageLoader
+import coil.request.Disposable
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaBinding
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
+import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
+import org.koitharu.kotatsu.utils.ext.enqueueWith
+import org.koitharu.kotatsu.utils.ext.newImageRequest
+import org.koitharu.kotatsu.utils.ext.textAndVisible
+
+fun searchSuggestionMangaAD(
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
+ listener: SearchSuggestionListener,
+) = adapterDelegateViewBinding(
+ { inflater, parent -> ItemSearchSuggestionMangaBinding.inflate(inflater, parent, false) }
+) {
+
+ var imageRequest: Disposable? = null
+
+ itemView.setOnClickListener {
+ listener.onMangaClick(item.manga)
+ }
+
+ bind {
+ imageRequest?.dispose()
+ imageRequest = binding.imageViewCover.newImageRequest(item.manga.coverUrl)
+ .placeholder(R.drawable.ic_placeholder)
+ .fallback(R.drawable.ic_placeholder)
+ .error(R.drawable.ic_placeholder)
+ .allowRgb565(true)
+ .lifecycle(lifecycleOwner)
+ .enqueueWith(coil)
+ binding.textViewTitle.text = item.manga.title
+ binding.textViewSubtitle.textAndVisible = item.manga.altTitle
+ }
+
+ onViewRecycled {
+ imageRequest?.dispose()
+ binding.imageViewCover.setImageDrawable(null)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt
new file mode 100644
index 000000000..70854cb2f
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt
@@ -0,0 +1,26 @@
+package org.koitharu.kotatsu.search.ui.suggestion.adapter
+
+import android.view.View
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryBinding
+import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
+import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
+
+fun searchSuggestionQueryAD(
+ listener: SearchSuggestionListener,
+) = adapterDelegateViewBinding(
+ { inflater, parent -> ItemSearchSuggestionQueryBinding.inflate(inflater, parent, false) }
+) {
+
+ val viewClickListener = View.OnClickListener { v ->
+ listener.onQueryClick(item.query, v.id != R.id.button_complete)
+ }
+
+ binding.root.setOnClickListener(viewClickListener)
+ binding.buttonComplete.setOnClickListener(viewClickListener)
+
+ bind {
+ binding.textViewTitle.text = item.query
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt
new file mode 100644
index 000000000..d2d9d04d7
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt
@@ -0,0 +1,21 @@
+package org.koitharu.kotatsu.search.ui.suggestion.model
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.koitharu.kotatsu.core.model.Manga
+import org.koitharu.kotatsu.core.model.MangaSource
+
+sealed class SearchSuggestionItem {
+
+ data class MangaItem(
+ val manga: Manga,
+ ) : SearchSuggestionItem()
+
+ data class RecentQuery(
+ val query: String,
+ ) : SearchSuggestionItem()
+
+ data class Header(
+ val source: MangaSource,
+ val isChecked: MutableStateFlow,
+ ) : SearchSuggestionItem()
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
index dfff7115f..a2710540e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
@@ -14,7 +14,7 @@ import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.Cache
-import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
+import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.FileSizeUtils
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
private val trackerRepo by inject(mode = LazyThreadSafetyMode.NONE)
+ private val searchRepository by inject(mode = LazyThreadSafetyMode.NONE)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_history)
@@ -49,7 +50,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
findPreference(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
viewLifecycleScope.launchWhenResumed {
- val items = MangaSuggestionsProvider.getItemsCount(pref.context)
+ val items = searchRepository.getSearchHistoryCount()
pref.summary =
pref.context.resources.getQuantityString(R.plurals.items, items, items)
}
@@ -87,7 +88,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
viewLifecycleScope.launch {
- MangaSuggestionsProvider.clearHistory(preference.context)
+ searchRepository.clearSearchHistory()
preference.summary = preference.context.resources
.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
index 9a0fe450b..c73cff93f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
@@ -16,6 +16,7 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
+import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
fun View.hideKeyboard() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
@@ -153,4 +154,8 @@ fun RecyclerView.findCenterViewPosition(): Int {
val centerY = height / 2f
val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION
return getChildAdapterPosition(view)
+}
+
+inline fun RecyclerView.ViewHolder.getItem(): T? {
+ return ((this as? AdapterDelegateViewBindingViewHolder<*, *>)?.item as? T)
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_clear_all.xml b/app/src/main/res/drawable/ic_clear_all.xml
new file mode 100644
index 000000000..99c92f173
--- /dev/null
+++ b/app/src/main/res/drawable/ic_clear_all.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_complete.xml b/app/src/main/res/drawable/ic_complete.xml
new file mode 100644
index 000000000..3735578eb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_complete.xml
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search_suggestion.xml b/app/src/main/res/layout/fragment_search_suggestion.xml
new file mode 100644
index 000000000..a2ff52eda
--- /dev/null
+++ b/app/src/main/res/layout/fragment_search_suggestion.xml
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_search_suggestion_header.xml b/app/src/main/res/layout/item_search_suggestion_header.xml
new file mode 100644
index 000000000..62b29b12d
--- /dev/null
+++ b/app/src/main/res/layout/item_search_suggestion_header.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_search_suggestion_manga.xml b/app/src/main/res/layout/item_search_suggestion_manga.xml
new file mode 100644
index 000000000..a92340c02
--- /dev/null
+++ b/app/src/main/res/layout/item_search_suggestion_manga.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_search_suggestion_query.xml b/app/src/main/res/layout/item_search_suggestion_query.xml
new file mode 100644
index 000000000..ace877682
--- /dev/null
+++ b/app/src/main/res/layout/item_search_suggestion_query.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 3765820e9..b20c30211 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -206,4 +206,5 @@
Confirm
Пароль должен содержать не менее 4 символов
Прятать заголовок при прокрутке
+ Поиск только по %s
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6f5e7da6f..64fb9d0bc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -208,4 +208,6 @@
Confirm
Password must be at least 4 characters
Hide toolbar when scrolling
+ Search only on %s
+ Do you really want to remove all recent search queries? This action cannot be undone.
\ No newline at end of file