Search in chapters #133

This commit is contained in:
Koitharu
2022-03-23 08:49:44 +02:00
parent d5c7d8997f
commit d3e9dc2ea4
16 changed files with 164 additions and 42 deletions

View File

@@ -66,7 +66,7 @@ android {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'com.github.nv95:kotatsu-parsers:fe243c8acf'
implementation 'com.github.nv95:kotatsu-parsers:e15dbf2a4b'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'

View File

@@ -7,11 +7,11 @@ import android.widget.AdapterView
import android.widget.Spinner
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDividerItemDecoration
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -27,10 +27,13 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem>,
ActionMode.Callback,
AdapterView.OnItemSelectedListener {
AdapterView.OnItemSelectedListener,
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener {
private val viewModel by sharedViewModel<DetailsViewModel>()
@@ -63,6 +66,10 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu()
}
viewModel.hasChapters.observe(viewLifecycleOwner) {
binding.textViewHolder.isGone = it
activity?.invalidateOptionsMenu()
}
}
override fun onDestroyView() {
@@ -75,11 +82,18 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_chapters, menu)
val searchMenuItem = menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search).isVisible = viewModel.hasChapters.value == true
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
@@ -117,7 +131,8 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
view.context,
viewModel.manga.value ?: return,
ReaderState(item.chapter.id, 0, 0)
), options.toBundle()
),
options.toBundle()
)
}
@@ -189,6 +204,21 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
actionMode = null
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
(item?.actionView as? SearchView)?.setQuery("", false)
viewModel.performChapterSearch(null)
return true
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performChapterSearch(newText)
return true
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerViewChapters.updatePadding(
bottom = insets.bottom + (binding.spinnerBranches?.height ?: 0),

View File

@@ -4,9 +4,7 @@ import android.app.ActivityOptions
import android.os.Bundle
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
@@ -38,12 +36,20 @@ 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, ChipsView.OnChipClickListener {
class DetailsFragment :
BaseFragment<FragmentDetailsBinding>(),
View.OnClickListener,
View.OnLongClickListener,
ChipsView.OnChipClickListener {
private val viewModel by sharedViewModel<DetailsViewModel>()
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -64,6 +70,11 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_details_info, menu)
}
private fun onMangaUpdated(manga: Manga) {
with(binding) {
// Main
@@ -276,4 +287,4 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
.lifecycle(viewLifecycleOwner)
.enqueueWith(coil)
}
}
}

View File

@@ -40,7 +40,7 @@ class DetailsViewModel(
) : BaseViewModel() {
private var loadingJob: Job
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
private val mangaData = MutableStateFlow(intent.manga)
private val selectedBranch = MutableStateFlow<String?>(null)
private val history = mangaData.mapNotNull { it?.id }
@@ -62,6 +62,7 @@ class DetailsViewModel(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val remoteManga = MutableStateFlow<Manga?>(null)
private val chaptersQuery = MutableStateFlow("")
private val chaptersReversed = settings.observe()
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
@@ -93,21 +94,29 @@ class DetailsViewModel(
branches.indexOf(selected)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val hasChapters = mangaData.map {
!(it?.chapters.isNullOrEmpty())
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val chapters = combine(
mangaData.map { it?.chapters.orEmpty() },
remoteManga,
history.map { it?.chapterId },
newChapters,
selectedBranch
) { chapters, sourceManga, currentId, newCount, branch ->
val sourceChapters = sourceManga?.chapters
if (sourceManga?.source != MangaSource.LOCAL && !sourceChapters.isNullOrEmpty()) {
mapChaptersWithSource(chapters, sourceChapters, currentId, newCount, branch)
} else {
mapChapters(chapters, sourceChapters, currentId, newCount, branch)
}
}.combine(chaptersReversed) { list, reversed ->
if (reversed) list.asReversed() else list
combine(
mangaData.map { it?.chapters.orEmpty() },
remoteManga,
history.map { it?.chapterId },
newChapters,
selectedBranch
) { chapters, sourceManga, currentId, newCount, branch ->
val sourceChapters = sourceManga?.chapters
if (sourceManga?.source != MangaSource.LOCAL && !sourceChapters.isNullOrEmpty()) {
mapChaptersWithSource(chapters, sourceChapters, currentId, newCount, branch)
} else {
mapChapters(chapters, sourceChapters, currentId, newCount, branch)
}
},
chaptersReversed,
chaptersQuery,
) { list, reversed, query ->
(if (reversed) list.asReversed() else list).filterSearch(query)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
init {
@@ -142,6 +151,10 @@ class DetailsViewModel(
return remoteManga.value
}
fun performChapterSearch(query: String?) {
chaptersQuery.value = query?.trim().orEmpty()
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
@@ -262,4 +275,13 @@ class DetailsViewModel(
}
return groups.maxByOrNull { it.value.size }?.key
}
}
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
if (query.isEmpty() || this.isEmpty()) {
return this
}
return filter {
it.chapter.name.contains(query, ignoreCase = true)
}
}
}

View File

@@ -5,8 +5,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDividerItemDecoration
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
@@ -35,9 +33,6 @@ class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemC
if (!resources.getBoolean(R.bool.is_tablet)) {
binding.toolbar.navigationIcon = null
}
binding.recyclerView.addItemDecoration(
MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL)
)
val chapters = arguments?.getParcelable<ParcelableMangaChapters>(ARG_CHAPTERS)?.chapters
if (chapters.isNullOrEmpty()) {
dismissAllowingStateLoss()

View File

@@ -66,6 +66,7 @@ fun pageThumbnailAD(
onViewRecycled {
job?.cancel()
job = null
binding.imageViewThumb.setImageDrawable(null)
}
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.databinding.DialogOnboardBinding
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import org.koitharu.kotatsu.utils.ext.observeNotNull
import org.koitharu.kotatsu.utils.ext.showAllowStateLoss
import org.koitharu.kotatsu.utils.ext.withArgs
class OnboardDialogFragment : AlertDialogFragment<DialogOnboardBinding>(),
@@ -77,7 +78,7 @@ class OnboardDialogFragment : AlertDialogFragment<DialogOnboardBinding>(),
fun showWelcome(fm: FragmentManager) {
OnboardDialogFragment().withArgs(1) {
putBoolean(ARG_WELCOME, true)
}.show(fm, TAG)
}.showAllowStateLoss(fm, TAG)
}
}
}

View File

@@ -2,7 +2,9 @@ package org.koitharu.kotatsu.utils.ext
import android.os.Bundle
import android.os.Parcelable
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.coroutineScope
import java.io.Serializable
@@ -34,4 +36,10 @@ inline fun <reified T : Serializable> Fragment.serializableArgument(name: String
fun Fragment.stringArgument(name: String) = lazy(LazyThreadSafetyMode.NONE) {
arguments?.getString(name)
}
fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) {
if (!manager.isStateSaved) {
show(manager, tag)
}
}

View File

@@ -25,4 +25,17 @@
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/margin_normal"
android:gravity="center"
android:text="@string/chapters_empty"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>

View File

@@ -37,8 +37,25 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:layout_below="@id/spinner_branches"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="@dimen/margin_normal"
android:gravity="center"
android:text="@string/chapters_empty"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="gone"
tools:visibility="visible" />

View File

@@ -3,6 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:orderInCategory="10"
android:title="@string/search_chapters"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_reversed"
android:checkable="true"

View File

@@ -3,13 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="@drawable/ic_share"
android:orderInCategory="10"
android:title="@string/share"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_save"
android:orderInCategory="40"

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="@drawable/ic_share"
android:orderInCategory="15"
android:title="@string/share"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -267,4 +267,6 @@
<string name="logged_in_as">Logged in as %s</string>
<string name="nsfw">18+</string>
<string name="various_languages">Various languages</string>
<string name="search_chapters">Find chapter</string>
<string name="chapters_empty">No chapters in this manga</string>
</resources>