From d3e9dc2ea4ab7cb50844c0cec82c6c1563d43288 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 23 Mar 2022 08:49:44 +0200 Subject: [PATCH] Search in chapters #133 --- .editorconfig | 1 + .idea/ktlint.xml | 7 +++ app/build.gradle | 2 +- .../kotatsu/details/ui/ChaptersFragment.kt | 40 ++++++++++++-- .../kotatsu/details/ui/DetailsFragment.kt | 23 +++++--- .../kotatsu/details/ui/DetailsViewModel.kt | 54 +++++++++++++------ .../kotatsu/reader/ui/ChaptersBottomSheet.kt | 5 -- .../ui/thumbnails/adapter/PageThumbnailAD.kt | 1 + .../settings/onboard/OnboardDialogFragment.kt | 3 +- .../koitharu/kotatsu/utils/ext/FragmentExt.kt | 8 +++ .../res/layout-w720dp/fragment_chapters.xml | 13 +++++ app/src/main/res/layout/fragment_chapters.xml | 19 ++++++- app/src/main/res/menu/opt_chapters.xml | 8 +++ app/src/main/res/menu/opt_details.xml | 7 --- app/src/main/res/menu/opt_details_info.xml | 13 +++++ app/src/main/res/values/strings.xml | 2 + 16 files changed, 164 insertions(+), 42 deletions(-) create mode 100644 .idea/ktlint.xml create mode 100644 app/src/main/res/menu/opt_details_info.xml diff --git a/.editorconfig b/.editorconfig index e52454a9d..00754a4a4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,7 @@ indent_style = tab insert_final_newline = false max_line_length = 120 tab_width = 4 +disabled_rules=no-wildcard-imports,no-unused-imports [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] ij_continuation_indent_size = 4 diff --git a/.idea/ktlint.xml b/.idea/ktlint.xml new file mode 100644 index 000000000..e1ecd151a --- /dev/null +++ b/.idea/ktlint.xml @@ -0,0 +1,7 @@ + + + + true + false + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 3dfb0d13f..1bdb3f201 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index d90b74d01..cd08c6139 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -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(), +class ChaptersFragment : + BaseFragment(), OnListItemClickListener, ActionMode.Callback, - AdapterView.OnItemSelectedListener { + AdapterView.OnItemSelectedListener, + MenuItem.OnActionExpandListener, + SearchView.OnQueryTextListener { private val viewModel by sharedViewModel() @@ -63,6 +66,10 @@ class ChaptersFragment : BaseFragment(), 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(), 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(), view.context, viewModel.manga.value ?: return, ReaderState(item.chapter.id, 0, 0) - ), options.toBundle() + ), + options.toBundle() ) } @@ -189,6 +204,21 @@ class ChaptersFragment : BaseFragment(), 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), diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 5acdfd109..2d182693d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -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(), View.OnClickListener, - View.OnLongClickListener, ChipsView.OnChipClickListener { +class DetailsFragment : + BaseFragment(), + View.OnClickListener, + View.OnLongClickListener, + ChipsView.OnChipClickListener { private val viewModel by sharedViewModel() private val coil by inject(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(), 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(), View.OnClickList .lifecycle(viewLifecycleOwner) .enqueueWith(coil) } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index e3101c734..074eefeb1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -40,7 +40,7 @@ class DetailsViewModel( ) : BaseViewModel() { private var loadingJob: Job - private val mangaData = MutableStateFlow(intent.manga) + private val mangaData = MutableStateFlow(intent.manga) private val selectedBranch = MutableStateFlow(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(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.filterSearch(query: String): List { + if (query.isEmpty() || this.isEmpty()) { + return this + } + return filter { + it.chapter.name.contains(query, ignoreCase = true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt index 33a5cc62c..49151127e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt @@ -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(), OnListItemC if (!resources.getBoolean(R.bool.is_tablet)) { binding.toolbar.navigationIcon = null } - binding.recyclerView.addItemDecoration( - MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL) - ) val chapters = arguments?.getParcelable(ARG_CHAPTERS)?.chapters if (chapters.isNullOrEmpty()) { dismissAllowingStateLoss() diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt index 6ddd03a91..416b520d2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt @@ -66,6 +66,7 @@ fun pageThumbnailAD( onViewRecycled { job?.cancel() + job = null binding.imageViewThumb.setImageDrawable(null) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt index 74dc3ffbe..005469b96 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -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(), @@ -77,7 +78,7 @@ class OnboardDialogFragment : AlertDialogFragment(), fun showWelcome(fm: FragmentManager) { OnboardDialogFragment().withArgs(1) { putBoolean(ARG_WELCOME, true) - }.show(fm, TAG) + }.showAllowStateLoss(fm, TAG) } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt index fd754cb83..cae599f8a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt @@ -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 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) + } } \ No newline at end of file diff --git a/app/src/main/res/layout-w720dp/fragment_chapters.xml b/app/src/main/res/layout-w720dp/fragment_chapters.xml index 386ce6fb0..7c4195c0d 100644 --- a/app/src/main/res/layout-w720dp/fragment_chapters.xml +++ b/app/src/main/res/layout-w720dp/fragment_chapters.xml @@ -25,4 +25,17 @@ android:visibility="gone" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chapters.xml b/app/src/main/res/layout/fragment_chapters.xml index 33508fba8..d05821f2a 100644 --- a/app/src/main/res/layout/fragment_chapters.xml +++ b/app/src/main/res/layout/fragment_chapters.xml @@ -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" /> + + diff --git a/app/src/main/res/menu/opt_chapters.xml b/app/src/main/res/menu/opt_chapters.xml index 11cd67383..f0bdc7b54 100644 --- a/app/src/main/res/menu/opt_chapters.xml +++ b/app/src/main/res/menu/opt_chapters.xml @@ -3,6 +3,14 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9e9aca80..995191d21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -267,4 +267,6 @@ Logged in as %s 18+ Various languages + Find chapter + No chapters in this manga \ No newline at end of file