Merge branch 'devel' into feature/desktop-ui

pull/127/head
Koitharu 4 years ago
commit 05ffc145be
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -50,6 +50,8 @@
<activity <activity
android:name="org.koitharu.kotatsu.search.ui.SearchActivity" android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:label="@string/search" /> android:label="@string/search" />
<activity android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
android:label="@string/search_manga" />
<activity <activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity" android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:label="@string/settings" /> android:label="@string/settings" />

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.base.ui package org.koitharu.kotatsu.base.ui
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -53,15 +52,6 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
super.onDestroyView() super.onDestroyView()
} }
open fun getTitle(): CharSequence? = null
override fun onAttach(context: Context) {
super.onAttach(context)
getTitle()?.let {
activity?.title = it
}
}
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat { override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
if (newInsets != lastInsets) { if (newInsets != lastInsets) {

@ -80,6 +80,7 @@ class ChipsView @JvmOverloads constructor(
chip.setOnCloseIconClickListener(chipOnCloseListener) chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false) chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener) chip.setOnClickListener(chipOnClickListener)
chip.isCheckable = false
addView(chip) addView(chip)
return chip return chip
} }

@ -9,6 +9,45 @@ abstract class TagsDao {
@Query("SELECT * FROM tags WHERE source = :source") @Query("SELECT * FROM tags WHERE source = :source")
abstract suspend fun findTags(source: String): List<TagEntity> abstract suspend fun findTags(source: String): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findPopularTags(source: String, limit: Int): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source AND title LIKE :query
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findTags(source: String, query: String, limit: Int): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE title LIKE :query
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(tag: TagEntity): Long abstract suspend fun insert(tag: TagEntity): Long

@ -16,27 +16,26 @@ import androidx.core.view.updatePadding
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaState
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener, class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener,
View.OnLongClickListener { View.OnLongClickListener, ChipsView.OnChipClickListener {
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by sharedViewModel<DetailsViewModel>()
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE) private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
@ -54,6 +53,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
binding.buttonRead.setOnLongClickListener(this) binding.buttonRead.setOnLongClickListener(this)
binding.imageViewCover.setOnClickListener(this) binding.imageViewCover.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
binding.chipsTags.onChipClickListener = this
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated) viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged) viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
@ -231,6 +231,11 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
} }
} }
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag ?: return
startActivity(MangaListActivity.newIntent(requireContext(), tag))
}
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding( binding.root.updatePadding(
bottom = insets.bottom, bottom = insets.bottom,
@ -242,7 +247,8 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
manga.tags.map { tag -> manga.tags.map { tag ->
ChipsView.ChipModel( ChipsView.ChipModel(
title = tag.title, title = tag.title,
icon = 0 icon = 0,
data = tag,
) )
} }
) )

@ -103,10 +103,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
override fun getTitle(): CharSequence? {
return context?.getString(R.string.favourites)
}
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
} }

@ -59,10 +59,6 @@ class HistoryListFragment : MangaListFragment() {
} }
} }
override fun getTitle(): CharSequence? {
return context?.getString(R.string.history)
}
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) { override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
super.onCreatePopupMenu(inflater, menu, data) super.onCreatePopupMenu(inflater, menu, data)
inflater.inflate(R.menu.popup_history, menu) inflater.inflate(R.menu.popup_history, menu)

@ -59,6 +59,12 @@ class FilterCoordinator(
} }
} }
fun setTags(tags: Set<MangaTag>) {
currentState.update { oldValue ->
FilterState(oldValue.sortOrder, tags)
}
}
fun reset() { fun reset() {
currentState.update { oldValue -> currentState.update { oldValue ->
FilterState(oldValue.sortOrder, emptySet()) FilterState(oldValue.sortOrder, emptySet())

@ -92,10 +92,6 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
} }
} }
override fun getTitle(): CharSequence? {
return context?.getString(R.string.local_storage)
}
override fun onActivityResult(result: List<@JvmSuppressWildcards Uri>) { override fun onActivityResult(result: List<@JvmSuppressWildcards Uri>) {
if (result.isEmpty()) return if (result.isEmpty()) return
viewModel.importFiles(result) viewModel.importFiles(result)

@ -33,6 +33,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.prefs.AppSection import org.koitharu.kotatsu.core.prefs.AppSection
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.databinding.ActivityMainBinding
@ -43,6 +44,7 @@ import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.local.ui.LocalListFragment import org.koitharu.kotatsu.local.ui.LocalListFragment
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
@ -261,6 +263,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
} }
} }
override fun onTagClick(tag: MangaTag) {
startActivity(
MangaListActivity.newIntent(this, tag)
)
}
override fun onQueryChanged(query: String) { override fun onQueryChanged(query: String) {
searchSuggestionViewModel.onQueryChanged(query) searchSuggestionViewModel.onQueryChanged(query)
} }
@ -353,17 +361,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment, TAG_PRIMARY) .replace(R.id.container, fragment, TAG_PRIMARY)
.commit() .commit()
if (fragment is HistoryListFragment) binding.fab.show() else binding.fab.hide() adjustFabVisibility(topFragment = fragment)
} }
private fun onSearchOpened() { private fun onSearchOpened() {
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
drawerToggle?.isDrawerIndicatorEnabled = false drawerToggle?.isDrawerIndicatorEnabled = false
adjustFabVisibility(isSearchOpened = true)
} }
private fun onSearchClosed() { private fun onSearchClosed() {
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
drawerToggle?.isDrawerIndicatorEnabled = true drawerToggle?.isDrawerIndicatorEnabled = true
adjustFabVisibility(isSearchOpened = false)
} }
private fun onFirstStart() { private fun onFirstStart() {
@ -378,4 +388,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
} }
} }
} }
private fun adjustFabVisibility(
topFragment: Fragment? = supportFragmentManager.findFragmentByTag(TAG_PRIMARY),
isSearchOpened: Boolean = supportFragmentManager.findFragmentByTag(TAG_SEARCH)?.isVisible == true,
) {
if (!isSearchOpened && topFragment is HistoryListFragment) binding.fab.show() else binding.fab.hide()
}
} }

@ -25,10 +25,6 @@ class RemoteListFragment : MangaListFragment() {
viewModel.loadNextPage() viewModel.loadNextPage()
} }
override fun getTitle(): CharSequence {
return source.title
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_list_remote, menu) inflater.inflate(R.menu.opt_list_remote, menu)

@ -110,6 +110,10 @@ class RemoteListViewModel(
fun resetFilter() = filter.reset() fun resetFilter() = filter.reset()
fun applyFilter(tags: Set<MangaTag>) {
filter.setTags(tags)
}
private fun loadList(filterState: FilterState, append: Boolean) { private fun loadList(filterState: FilterState, append: Boolean) {
if (loadingJob?.isActive == true) { if (loadingJob?.isActive == true) {
return return

@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@ -79,6 +80,17 @@ class MangaSearchRepository(
}.orEmpty() }.orEmpty()
} }
suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> {
return when {
query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit)
query.isNotEmpty() -> db.tagsDao.findTags("%$query%", limit)
source != null -> db.tagsDao.findTags(source.name, limit)
else -> db.tagsDao.findPopularTags(limit)
}.map {
it.toMangaTag()
}
}
fun saveSearchQuery(query: String) { fun saveSearchQuery(query: String) {
recentSuggestions.saveRecentQuery(query, null) recentSuggestions.saveRecentQuery(query, null)
} }

@ -0,0 +1,77 @@
package org.koitharu.kotatsu.search.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import org.koin.androidx.viewmodel.ext.android.getViewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
class MangaListActivity : BaseActivity<ActivitySearchGlobalBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySearchGlobalBinding.inflate(layoutInflater))
val tag = intent.getParcelableExtra<MangaTag>(EXTRA_TAG) ?: run {
finishAfterTransition()
return
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
val fragment = RemoteListFragment.newInstance(tag.source)
replace(R.id.container, fragment)
runOnCommit(ApplyFilterRunnable(fragment, tag))
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
with(binding.toolbar) {
updatePadding(
left = insets.left,
right = insets.right
)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
binding.container.updatePadding(
bottom = insets.bottom
)
}
private class ApplyFilterRunnable(
private val fragment: Fragment,
private val tag: MangaTag,
) : Runnable {
override fun run() {
val viewModel = fragment.getViewModel<RemoteListViewModel> {
parametersOf(tag.source)
}
viewModel.applyFilter(setOf(tag))
}
}
companion object {
private const val EXTRA_TAG = "tag"
fun newIntent(context: Context, tag: MangaTag) =
Intent(context, MangaListActivity::class.java)
.putExtra(EXTRA_TAG, tag)
}
}

@ -21,8 +21,6 @@ class SearchFragment : MangaListFragment() {
viewModel.loadNextPage() viewModel.loadNextPage()
} }
override fun getTitle() = query
companion object { companion object {
private const val ARG_QUERY = "query" private const val ARG_QUERY = "query"

@ -17,10 +17,6 @@ class GlobalSearchFragment : MangaListFragment() {
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
override fun getTitle(): CharSequence? {
return query
}
companion object { companion object {
private const val ARG_QUERY = "query" private const val ARG_QUERY = "query"

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.search.ui.suggestion
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter import org.koitharu.kotatsu.search.ui.suggestion.adapter.SEARCH_SUGGESTION_ITEM_TYPE_QUERY
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import org.koitharu.kotatsu.utils.ext.getItem import org.koitharu.kotatsu.utils.ext.getItem
@ -18,7 +18,7 @@ class SearchSuggestionItemCallback(
override fun getMovementFlags( override fun getMovementFlags(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
): Int = if (viewHolder.itemViewType == SearchSuggestionAdapter.ITEM_TYPE_QUERY) { ): Int = if (viewHolder.itemViewType == SEARCH_SUGGESTION_ITEM_TYPE_QUERY) {
movementFlags movementFlags
} else { } else {
0 0

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.search.ui.suggestion package org.koitharu.kotatsu.search.ui.suggestion
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaTag
interface SearchSuggestionListener { interface SearchSuggestionListener {
@ -11,4 +12,6 @@ interface SearchSuggestionListener {
fun onQueryChanged(query: String) fun onQueryChanged(query: String)
fun onClearSearchHistory() fun onClearSearchHistory()
fun onTagClick(tag: MangaTag)
} }

@ -2,20 +2,19 @@ package org.koitharu.kotatsu.search.ui.suggestion
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
private const val DEBOUNCE_TIMEOUT = 500L private const val DEBOUNCE_TIMEOUT = 500L
private const val SEARCH_THRESHOLD = 3 private const val MAX_MANGA_ITEMS = 6
private const val MAX_MANGA_ITEMS = 3 private const val MAX_QUERY_ITEMS = 16
private const val MAX_QUERY_ITEMS = 120 private const val MAX_TAGS_ITEMS = 8
private const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 1
class SearchSuggestionViewModel( class SearchSuggestionViewModel(
private val repository: MangaSearchRepository, private val repository: MangaSearchRepository,
@ -65,28 +64,56 @@ class SearchSuggestionViewModel(
private fun setupSuggestion() { private fun setupSuggestion() {
suggestionJob?.cancel() suggestionJob?.cancel()
suggestionJob = combine( suggestionJob = combine(
query query.debounce(DEBOUNCE_TIMEOUT),
.debounce(DEBOUNCE_TIMEOUT)
.mapLatest { q ->
q to repository.getQuerySuggestion(q, MAX_QUERY_ITEMS)
},
source, source,
isLocalSearch isLocalSearch,
) { (q, queries), src, srcOnly -> ::Triple,
val result = ArrayList<SearchSuggestionItem>(MAX_SUGGESTION_ITEMS) ).mapLatest { (searchQuery, src, srcOnly) ->
buildSearchSuggestion(searchQuery, src, srcOnly)
}.distinctUntilChanged()
.onEach {
suggestion.postValue(it)
}.launchIn(viewModelScope + Dispatchers.Default)
}
private suspend fun buildSearchSuggestion(
searchQuery: String,
src: MangaSource?,
srcOnly: Boolean,
): List<SearchSuggestionItem> = coroutineScope {
val queriesDeferred = async {
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
}
val tagsDeferred = async {
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, src.takeIf { srcOnly })
}
val mangaDeferred = async {
repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
}
val tags = tagsDeferred.await()
val mangaList = mangaDeferred.await()
val queries = queriesDeferred.await()
buildList(queries.size + 3) {
if (src != null) { if (src != null) {
result += SearchSuggestionItem.Header(src, isLocalSearch) add(SearchSuggestionItem.Header(src, isLocalSearch))
}
if (tags.isNotEmpty()) {
add(SearchSuggestionItem.Tags(mapTags(tags)))
} }
if (q.length >= SEARCH_THRESHOLD) { if (mangaList.isNotEmpty()) {
repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly }) add(SearchSuggestionItem.MangaList(mangaList))
.mapTo(result) {
SearchSuggestionItem.MangaItem(it)
}
} }
queries.mapTo(result) { SearchSuggestionItem.RecentQuery(it) } queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
result }
}.onEach { }
suggestion.postValue(it)
}.launchIn(viewModelScope + Dispatchers.Default) private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
ChipsView.ChipModel(
icon = 0,
title = tag.title,
data = tag,
)
} }
} }

@ -8,6 +8,8 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import kotlin.jvm.internal.Intrinsics import kotlin.jvm.internal.Intrinsics
const val SEARCH_SUGGESTION_ITEM_TYPE_QUERY = 0
class SearchSuggestionAdapter( class SearchSuggestionAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
@ -15,9 +17,11 @@ class SearchSuggestionAdapter(
) : AsyncListDifferDelegationAdapter<SearchSuggestionItem>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<SearchSuggestionItem>(DiffCallback()) {
init { init {
delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener)) delegatesManager
.addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener)) .addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
.addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener)) .addDelegate(searchSuggestionHeaderAD(listener))
.addDelegate(searchSuggestionTagsAD(listener))
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
} }
private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() { private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() {
@ -26,14 +30,10 @@ class SearchSuggestionAdapter(
oldItem: SearchSuggestionItem, oldItem: SearchSuggestionItem,
newItem: SearchSuggestionItem, newItem: SearchSuggestionItem,
): Boolean = when { ): 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 is SearchSuggestionItem.RecentQuery && newItem is SearchSuggestionItem.RecentQuery -> {
oldItem.query == newItem.query oldItem.query == newItem.query
} }
oldItem is SearchSuggestionItem.Header && newItem is SearchSuggestionItem.Header -> true else -> oldItem.javaClass == newItem.javaClass
else -> false
} }
override fun areContentsTheSame( override fun areContentsTheSame(
@ -41,11 +41,4 @@ class SearchSuggestionAdapter(
newItem: SearchSuggestionItem, newItem: SearchSuggestionItem,
): Boolean = Intrinsics.areEqual(oldItem, newItem) ): 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
}
} }

@ -1,46 +0,0 @@
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<SearchSuggestionItem.MangaItem, SearchSuggestionItem, ItemSearchSuggestionMangaBinding>(
{ 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)
}
}

@ -0,0 +1,23 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionTagsAD(
listener: SearchSuggestionListener,
) = adapterDelegate<SearchSuggestionItem.Tags, SearchSuggestionItem>(R.layout.item_search_suggestion_tags) {
val chipGroup = itemView as ChipsView
chipGroup.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
listener.onTagClick(data as? MangaTag ?: return@OnChipClickListener)
}
bind {
chipGroup.setChips(item.tags)
}
}

@ -0,0 +1,89 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.core.view.updatePadding
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import coil.request.Disposable
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaGridBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import org.koitharu.kotatsu.utils.ScrollResetCallback
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
fun searchSuggestionMangaListAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) = adapterDelegate<SearchSuggestionItem.MangaList, SearchSuggestionItem>(R.layout.item_search_suggestion_manga_list) {
val adapter = AsyncListDifferDelegationAdapter(
SuggestionMangaDiffCallback(),
searchSuggestionMangaGridAD(coil, lifecycleOwner, listener),
)
val recyclerView = itemView as RecyclerView
recyclerView.adapter = adapter
val spacing = context.resources.getDimensionPixelOffset(R.dimen.search_suggestions_manga_spacing)
recyclerView.updatePadding(
left = recyclerView.paddingLeft - spacing,
right = recyclerView.paddingRight - spacing,
)
recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
val scrollResetCallback = ScrollResetCallback(recyclerView)
bind {
adapter.setItems(item.items, scrollResetCallback)
}
}
private fun searchSuggestionMangaGridAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) = adapterDelegateViewBinding<Manga, Manga, ItemSearchSuggestionMangaGridBinding>(
{ layoutInflater, parent -> ItemSearchSuggestionMangaGridBinding.inflate(layoutInflater, parent, false) }
) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
listener.onMangaClick(item)
}
bind {
imageRequest?.dispose()
imageRequest = binding.imageViewCover.newImageRequest(item.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.title
}
onViewRecycled {
imageRequest?.dispose()
binding.imageViewCover.setImageDrawable(null)
}
}
private class SuggestionMangaDiffCallback : DiffUtil.ItemCallback<Manga>() {
override fun areItemsTheSame(oldItem: Manga, newItem: Manga): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Manga, newItem: Manga): Boolean {
return oldItem.title == newItem.title && oldItem.coverUrl == newItem.coverUrl
}
}

@ -1,21 +1,98 @@
package org.koitharu.kotatsu.search.ui.suggestion.model package org.koitharu.kotatsu.search.ui.suggestion.model
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.utils.ext.areItemsEquals
sealed class SearchSuggestionItem { sealed interface SearchSuggestionItem {
data class MangaItem( class MangaList(
val manga: Manga, val items: List<Manga>,
) : SearchSuggestionItem() ) : SearchSuggestionItem {
data class RecentQuery( override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaList
return items.areItemsEquals(other.items) { a, b ->
a.title == b.title && a.coverUrl == b.coverUrl
}
}
override fun hashCode(): Int {
return items.fold(0) { acc, t ->
var r = 31 * acc + t.title.hashCode()
r = 31 * r + t.coverUrl.hashCode()
r
}
}
}
class RecentQuery(
val query: String, val query: String,
) : SearchSuggestionItem() ) : SearchSuggestionItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
data class Header( other as RecentQuery
if (query != other.query) return false
return true
}
override fun hashCode(): Int {
return query.hashCode()
}
}
class Header(
val source: MangaSource, val source: MangaSource,
val isChecked: MutableStateFlow<Boolean>, val isChecked: MutableStateFlow<Boolean>,
) : SearchSuggestionItem() ) : SearchSuggestionItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Header
if (source != other.source) return false
if (isChecked !== other.isChecked) return false
return true
}
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + isChecked.hashCode()
return result
}
}
class Tags(
val tags: List<ChipsView.ChipModel>,
) : SearchSuggestionItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Tags
if (tags != other.tags) return false
return true
}
override fun hashCode(): Int {
return tags.hashCode()
}
}
} }

@ -1,14 +1,19 @@
package org.koitharu.kotatsu.search.ui.widget package org.koitharu.kotatsu.search.ui.widget
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
import androidx.core.content.ContextCompat
import com.google.android.material.R import com.google.android.material.R
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
private const val DRAWABLE_END = 2
class SearchEditText @JvmOverloads constructor( class SearchEditText @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@ -16,6 +21,7 @@ class SearchEditText @JvmOverloads constructor(
) : AppCompatEditText(context, attrs, defStyleAttr) { ) : AppCompatEditText(context, attrs, defStyleAttr) {
var searchSuggestionListener: SearchSuggestionListener? = null var searchSuggestionListener: SearchSuggestionListener? = null
private val clearIcon = ContextCompat.getDrawable(context, R.drawable.abc_ic_clear_material)
var query: String var query: String
get() = text?.trim()?.toString().orEmpty() get() = text?.trim()?.toString().orEmpty()
@ -50,9 +56,32 @@ class SearchEditText @JvmOverloads constructor(
lengthAfter: Int, lengthAfter: Int,
) { ) {
super.onTextChanged(text, start, lengthBefore, lengthAfter) super.onTextChanged(text, start, lengthBefore, lengthAfter)
setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
if (text.isNullOrEmpty()) null else clearIcon,
null,
)
searchSuggestionListener?.onQueryChanged(query) searchSuggestionListener?.onQueryChanged(query)
} }
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP) {
val drawable = compoundDrawablesRelative[DRAWABLE_END] ?: return super.onTouchEvent(event)
val isOnDrawable = drawable.isVisible && if (layoutDirection == LAYOUT_DIRECTION_RTL) {
event.x.toInt() in paddingLeft..(drawable.bounds.width() + paddingLeft)
} else {
event.x.toInt() in (width - drawable.bounds.width() - paddingRight)..(width - paddingRight)
}
if (isOnDrawable) {
text?.clear()
return true
}
}
return super.onTouchEvent(event)
}
override fun clearFocus() { override fun clearFocus() {
super.clearFocus() super.clearFocus()
text?.clear() text?.clear()

@ -46,10 +46,6 @@ class SuggestionsFragment : MangaListFragment() {
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
override fun getTitle(): CharSequence? {
return context?.getString(R.string.suggestions)
}
companion object { companion object {
fun newInstance() = SuggestionsFragment() fun newInstance() = SuggestionsFragment()

@ -35,8 +35,6 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
private var paddingVertical = 0 private var paddingVertical = 0
private var paddingHorizontal = 0 private var paddingHorizontal = 0
override fun getTitle() = context?.getString(R.string.updates)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

@ -0,0 +1,13 @@
package org.koitharu.kotatsu.utils
import androidx.recyclerview.widget.RecyclerView
import java.lang.ref.WeakReference
class ScrollResetCallback(recyclerView: RecyclerView) : Runnable {
private val recyclerViewRef = WeakReference(recyclerView)
override fun run() {
recyclerViewRef.get()?.scrollToPosition(0)
}
}

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import android.util.SparseArray
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
@ -82,4 +81,18 @@ fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
} else { } else {
Collections.rotate(subList(targetIndex, sourceIndex + 1), 1) Collections.rotate(subList(targetIndex, sourceIndex + 1), 1)
} }
}
inline fun <T> List<T>.areItemsEquals(other: List<T>, equals: (T, T) -> Boolean): Boolean {
if (size != other.size) {
return false
}
for (i in indices) {
val a = this[i]
val b = other[i]
if (!equals(a, b)) {
return false
}
}
return true
} }

@ -48,12 +48,15 @@
style="@style/Widget.Kotatsu.SearchView" style="@style/Widget.Kotatsu.SearchView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginEnd="2dp"
android:background="@null" android:background="@null"
android:gravity="center_vertical" android:gravity="center_vertical"
android:hint="@string/search_manga" android:hint="@string/search_manga"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:importantForAutofill="no" android:importantForAutofill="no"
android:singleLine="true" /> android:paddingBottom="1dp"
android:singleLine="true"
tools:drawableEnd="@drawable/abc_ic_clear_material" />
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="2dp"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingBottom="2dp">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="42dp"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[6]" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:textColorSecondary"
tools:text="@tools:sample/lorem[6]" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.CardView.Outlined"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:contentPadding="4dp"
tools:layout_height="@dimen/search_suggestions_manga_height">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
android:orientation="vertical"
android:scaleType="centerCrop"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elegantTextHeight="false"
android:ellipsize="end"
android:lines="1"
android:textAppearance="?attr/textAppearanceLabelSmall"
tools:text="@tools:sample/lorem[6]" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:scrollbars="none"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_search_suggestion_manga_grid"
android:layout_height="@dimen/search_suggestions_manga_height" />

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingVertical="4dp"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp" />

@ -18,4 +18,7 @@
<dimen name="list_footer_height_inner">36dp</dimen> <dimen name="list_footer_height_inner">36dp</dimen>
<dimen name="list_footer_height_outer">48dp</dimen> <dimen name="list_footer_height_outer">48dp</dimen>
<dimen name="screen_padding">16dp</dimen> <dimen name="screen_padding">16dp</dimen>
<dimen name="search_suggestions_manga_height">124dp</dimen>
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
</resources> </resources>

@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!--Toolbars--> <!--Toolbars-->
@ -76,6 +76,10 @@
<item name="elevationOverlayEnabled">@bool/elevation_overlay_enabled</item> <item name="elevationOverlayEnabled">@bool/elevation_overlay_enabled</item>
</style> </style>
<style name="ThemeOverlay.Kotatsu.MainToolbar" parent="">
<item name="colorControlHighlight">@color/selector_overlay</item>
</style>
<!-- TextAppearance --> <!-- TextAppearance -->
<style name="TextAppearance.Widget.Menu" parent="TextAppearance.AppCompat.Menu"> <style name="TextAppearance.Widget.Menu" parent="TextAppearance.AppCompat.Menu">
@ -100,6 +104,10 @@
<item name="cornerSize">12dp</item> <item name="cornerSize">12dp</item>
</style> </style>
<style name="ShapeAppearanceOverlay.Kotatsu.Cover.Small" parent="">
<item name="cornerSize">6dp</item>
</style>
<!--Preferences--> <!--Preferences-->
<style name="PreferenceThemeOverlay.Kotatsu"> <style name="PreferenceThemeOverlay.Kotatsu">

Loading…
Cancel
Save