Merge branch 'devel' into feature/desktop-ui
This commit is contained in:
@@ -50,6 +50,8 @@
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
||||
android:label="@string/search_manga" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -53,15 +52,6 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
|
||||
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 {
|
||||
val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
if (newInsets != lastInsets) {
|
||||
|
||||
@@ -80,6 +80,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
chip.setOnClickListener(chipOnClickListener)
|
||||
chip.isCheckable = false
|
||||
addView(chip)
|
||||
return chip
|
||||
}
|
||||
|
||||
@@ -9,6 +9,45 @@ abstract class TagsDao {
|
||||
@Query("SELECT * FROM tags WHERE source = :source")
|
||||
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)
|
||||
abstract suspend fun insert(tag: TagEntity): Long
|
||||
|
||||
|
||||
@@ -16,27 +16,26 @@ import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
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.core.model.*
|
||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
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.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
View.OnLongClickListener, ChipsView.OnChipClickListener {
|
||||
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
|
||||
@@ -54,6 +53,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
binding.buttonRead.setOnLongClickListener(this)
|
||||
binding.imageViewCover.setOnClickListener(this)
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
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) {
|
||||
binding.root.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
@@ -242,7 +247,8 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
manga.tags.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
icon = 0
|
||||
icon = 0,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -103,10 +103,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return context?.getString(R.string.favourites)
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
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) {
|
||||
super.onCreatePopupMenu(inflater, menu, data)
|
||||
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() {
|
||||
currentState.update { oldValue ->
|
||||
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>) {
|
||||
if (result.isEmpty()) return
|
||||
viewModel.importFiles(result)
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
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.AppSettings
|
||||
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.reader.ui.ReaderActivity
|
||||
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.global.GlobalSearchActivity
|
||||
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) {
|
||||
searchSuggestionViewModel.onQueryChanged(query)
|
||||
}
|
||||
@@ -353,17 +361,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, fragment, TAG_PRIMARY)
|
||||
.commit()
|
||||
if (fragment is HistoryListFragment) binding.fab.show() else binding.fab.hide()
|
||||
adjustFabVisibility(topFragment = fragment)
|
||||
}
|
||||
|
||||
private fun onSearchOpened() {
|
||||
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
drawerToggle?.isDrawerIndicatorEnabled = false
|
||||
adjustFabVisibility(isSearchOpened = true)
|
||||
}
|
||||
|
||||
private fun onSearchClosed() {
|
||||
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
drawerToggle?.isDrawerIndicatorEnabled = true
|
||||
adjustFabVisibility(isSearchOpened = false)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence {
|
||||
return source.title
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_list_remote, menu)
|
||||
|
||||
@@ -110,6 +110,10 @@ class RemoteListViewModel(
|
||||
|
||||
fun resetFilter() = filter.reset()
|
||||
|
||||
fun applyFilter(tags: Set<MangaTag>) {
|
||||
filter.setTags(tags)
|
||||
}
|
||||
|
||||
private fun loadList(filterState: FilterState, append: Boolean) {
|
||||
if (loadingJob?.isActive == true) {
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
|
||||
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.MangaTag
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -79,6 +80,17 @@ class MangaSearchRepository(
|
||||
}.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) {
|
||||
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()
|
||||
}
|
||||
|
||||
override fun getTitle() = query
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_QUERY = "query"
|
||||
|
||||
@@ -17,10 +17,6 @@ class GlobalSearchFragment : MangaListFragment() {
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return query
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
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.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.utils.ext.getItem
|
||||
|
||||
@@ -18,7 +18,7 @@ class SearchSuggestionItemCallback(
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
): Int = if (viewHolder.itemViewType == SearchSuggestionAdapter.ITEM_TYPE_QUERY) {
|
||||
): Int = if (viewHolder.itemViewType == SEARCH_SUGGESTION_ITEM_TYPE_QUERY) {
|
||||
movementFlags
|
||||
} else {
|
||||
0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.search.ui.suggestion
|
||||
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
|
||||
interface SearchSuggestionListener {
|
||||
|
||||
@@ -11,4 +12,6 @@ interface SearchSuggestionListener {
|
||||
fun onQueryChanged(query: String)
|
||||
|
||||
fun onClearSearchHistory()
|
||||
|
||||
fun onTagClick(tag: MangaTag)
|
||||
}
|
||||
@@ -2,20 +2,19 @@ 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.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.plus
|
||||
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.MangaTag
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
|
||||
private const val DEBOUNCE_TIMEOUT = 500L
|
||||
private const val SEARCH_THRESHOLD = 3
|
||||
private const val MAX_MANGA_ITEMS = 3
|
||||
private const val MAX_QUERY_ITEMS = 120
|
||||
private const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 1
|
||||
private const val MAX_MANGA_ITEMS = 6
|
||||
private const val MAX_QUERY_ITEMS = 16
|
||||
private const val MAX_TAGS_ITEMS = 8
|
||||
|
||||
class SearchSuggestionViewModel(
|
||||
private val repository: MangaSearchRepository,
|
||||
@@ -65,28 +64,56 @@ class SearchSuggestionViewModel(
|
||||
private fun setupSuggestion() {
|
||||
suggestionJob?.cancel()
|
||||
suggestionJob = combine(
|
||||
query
|
||||
.debounce(DEBOUNCE_TIMEOUT)
|
||||
.mapLatest { q ->
|
||||
q to repository.getQuerySuggestion(q, MAX_QUERY_ITEMS)
|
||||
},
|
||||
query.debounce(DEBOUNCE_TIMEOUT),
|
||||
source,
|
||||
isLocalSearch
|
||||
) { (q, queries), src, srcOnly ->
|
||||
val result = ArrayList<SearchSuggestionItem>(MAX_SUGGESTION_ITEMS)
|
||||
isLocalSearch,
|
||||
::Triple,
|
||||
).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) {
|
||||
result += SearchSuggestionItem.Header(src, isLocalSearch)
|
||||
add(SearchSuggestionItem.Header(src, isLocalSearch))
|
||||
}
|
||||
if (q.length >= SEARCH_THRESHOLD) {
|
||||
repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
|
||||
.mapTo(result) {
|
||||
SearchSuggestionItem.MangaItem(it)
|
||||
}
|
||||
if (tags.isNotEmpty()) {
|
||||
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||
}
|
||||
queries.mapTo(result) { SearchSuggestionItem.RecentQuery(it) }
|
||||
result
|
||||
}.onEach {
|
||||
suggestion.postValue(it)
|
||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||
if (mangaList.isNotEmpty()) {
|
||||
add(SearchSuggestionItem.MangaList(mangaList))
|
||||
}
|
||||
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||
}
|
||||
}
|
||||
|
||||
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 kotlin.jvm.internal.Intrinsics
|
||||
|
||||
const val SEARCH_SUGGESTION_ITEM_TYPE_QUERY = 0
|
||||
|
||||
class SearchSuggestionAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
@@ -15,9 +17,11 @@ class SearchSuggestionAdapter(
|
||||
) : AsyncListDifferDelegationAdapter<SearchSuggestionItem>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener))
|
||||
.addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
||||
.addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener))
|
||||
delegatesManager
|
||||
.addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
||||
.addDelegate(searchSuggestionHeaderAD(listener))
|
||||
.addDelegate(searchSuggestionTagsAD(listener))
|
||||
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() {
|
||||
@@ -26,14 +30,10 @@ class SearchSuggestionAdapter(
|
||||
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
|
||||
else -> oldItem.javaClass == newItem.javaClass
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
@@ -41,11 +41,4 @@ class SearchSuggestionAdapter(
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
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.MangaSource
|
||||
import org.koitharu.kotatsu.utils.ext.areItemsEquals
|
||||
|
||||
sealed class SearchSuggestionItem {
|
||||
sealed interface SearchSuggestionItem {
|
||||
|
||||
data class MangaItem(
|
||||
val manga: Manga,
|
||||
) : SearchSuggestionItem()
|
||||
class MangaList(
|
||||
val items: List<Manga>,
|
||||
) : 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,
|
||||
) : SearchSuggestionItem()
|
||||
) : SearchSuggestionItem {
|
||||
|
||||
data class Header(
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as RecentQuery
|
||||
|
||||
if (query != other.query) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return query.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
class Header(
|
||||
val source: MangaSource,
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.R
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
|
||||
private const val DRAWABLE_END = 2
|
||||
|
||||
class SearchEditText @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -16,6 +21,7 @@ class SearchEditText @JvmOverloads constructor(
|
||||
) : AppCompatEditText(context, attrs, defStyleAttr) {
|
||||
|
||||
var searchSuggestionListener: SearchSuggestionListener? = null
|
||||
private val clearIcon = ContextCompat.getDrawable(context, R.drawable.abc_ic_clear_material)
|
||||
|
||||
var query: String
|
||||
get() = text?.trim()?.toString().orEmpty()
|
||||
@@ -50,9 +56,32 @@ class SearchEditText @JvmOverloads constructor(
|
||||
lengthAfter: Int,
|
||||
) {
|
||||
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
if (text.isNullOrEmpty()) null else clearIcon,
|
||||
null,
|
||||
)
|
||||
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() {
|
||||
super.clearFocus()
|
||||
text?.clear()
|
||||
|
||||
@@ -46,10 +46,6 @@ class SuggestionsFragment : MangaListFragment() {
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return context?.getString(R.string.suggestions)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = SuggestionsFragment()
|
||||
|
||||
@@ -35,8 +35,6 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
|
||||
private var paddingVertical = 0
|
||||
private var paddingHorizontal = 0
|
||||
|
||||
override fun getTitle() = context?.getString(R.string.updates)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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
|
||||
|
||||
import android.util.SparseArray
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.collection.LongSparseArray
|
||||
@@ -82,4 +81,18 @@ fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
|
||||
} else {
|
||||
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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:background="@null"
|
||||
android:gravity="center_vertical"
|
||||
android:hint="@string/search_manga"
|
||||
android:imeOptions="actionSearch"
|
||||
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>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
11
app/src/main/res/layout/item_search_suggestion_tags.xml
Normal file
11
app/src/main/res/layout/item_search_suggestion_tags.xml
Normal file
@@ -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_outer">48dp</dimen>
|
||||
<dimen name="screen_padding">16dp</dimen>
|
||||
|
||||
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
||||
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<resources>
|
||||
|
||||
<!--Toolbars-->
|
||||
|
||||
@@ -76,6 +76,10 @@
|
||||
<item name="elevationOverlayEnabled">@bool/elevation_overlay_enabled</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.Kotatsu.MainToolbar" parent="">
|
||||
<item name="colorControlHighlight">@color/selector_overlay</item>
|
||||
</style>
|
||||
|
||||
<!-- TextAppearance -->
|
||||
|
||||
<style name="TextAppearance.Widget.Menu" parent="TextAppearance.AppCompat.Menu">
|
||||
@@ -100,6 +104,10 @@
|
||||
<item name="cornerSize">12dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Kotatsu.Cover.Small" parent="">
|
||||
<item name="cornerSize">6dp</item>
|
||||
</style>
|
||||
|
||||
<!--Preferences-->
|
||||
|
||||
<style name="PreferenceThemeOverlay.Kotatsu">
|
||||
|
||||
Reference in New Issue
Block a user