From 3fae457ec6bbdb96bc25303ddc73a2f9b55b0d3a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 14 Mar 2020 13:47:24 +0200 Subject: [PATCH] Show manga list by author --- .../kotatsu/ui/common/ChipsFactory.kt | 5 +- .../ui/details/MangaDetailsFragment.kt | 21 +- .../kotatsu/ui/main/list/MangaListSheet.kt | 196 ++++++++++++++++++ .../kotatsu/ui/search/MangaSearchSheet.kt | 50 +++++ app/src/main/res/layout/sheet_list.xml | 77 +++++++ app/src/main/res/menu/opt_list_sheet.xml | 12 ++ 6 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListSheet.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/search/MangaSearchSheet.kt create mode 100644 app/src/main/res/layout/sheet_list.xml create mode 100644 app/src/main/res/menu/opt_list_sheet.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/ChipsFactory.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/ChipsFactory.kt index f06ee59ba..0de8abf3b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/ChipsFactory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/ChipsFactory.kt @@ -1,13 +1,15 @@ package org.koitharu.kotatsu.ui.common import android.content.Context +import android.view.View import androidx.annotation.DrawableRes import com.google.android.material.chip.Chip import org.koitharu.kotatsu.utils.ext.getThemeColor class ChipsFactory(private val context: Context) { - fun create(convertView: Chip? = null, text: CharSequence, @DrawableRes iconRes: Int = 0, tag: Any? = null): Chip { + fun create(convertView: Chip? = null, text: CharSequence, @DrawableRes iconRes: Int = 0, + tag: Any? = null, onClickListener: View.OnClickListener? = null): Chip { val chip = convertView ?: Chip(context).apply { setTextColor(context.getThemeColor(android.R.attr.textColorPrimary)) isCloseIconVisible = false @@ -20,6 +22,7 @@ class ChipsFactory(private val context: Context) { chip.setChipIconResource(iconRes) } chip.tag = tag + chip.setOnClickListener(onClickListener) return chip } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsFragment.kt index 3005f4b86..8947328f7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsFragment.kt @@ -1,9 +1,11 @@ package org.koitharu.kotatsu.ui.details import android.text.Spanned +import android.view.View import androidx.core.text.parseAsHtml import androidx.core.view.isVisible import coil.api.load +import com.google.android.material.chip.Chip import kotlinx.android.synthetic.main.fragment_details.* import moxy.ktx.moxyPresenter import org.koitharu.kotatsu.R @@ -13,11 +15,12 @@ import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesDialog import org.koitharu.kotatsu.ui.reader.ReaderActivity +import org.koitharu.kotatsu.ui.search.MangaSearchSheet import org.koitharu.kotatsu.utils.ext.addChips import org.koitharu.kotatsu.utils.ext.textAndVisible import kotlin.math.roundToInt -class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView { +class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView, View.OnClickListener { @Suppress("unused") private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) @@ -47,7 +50,9 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai chips_tags.addChips(listOf(a)) { create( text = it, - iconRes = R.drawable.ic_chip_user + iconRes = R.drawable.ic_chip_user, + tag = it, + onClickListener = this@MangaDetailsFragment ) } } @@ -55,7 +60,8 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai create( text = it.title, iconRes = R.drawable.ic_chip_tag, - tag = it + tag = it, + onClickListener = this@MangaDetailsFragment ) } imageView_favourite.setOnClickListener { @@ -87,6 +93,15 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai override fun onMangaRemoved(manga: Manga) = Unit //handled in activity + override fun onClick(v: View) { + if (v is Chip) { + when(val tag = v.tag) { + is String -> MangaSearchSheet.show(activity?.supportFragmentManager ?: childFragmentManager, + manga?.source ?: return, tag) + } + } + } + private fun updateReadButton() { if (manga?.chapters.isNullOrEmpty()) { button_read.isEnabled = false diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListSheet.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListSheet.kt new file mode 100644 index 000000000..46afb54b8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListSheet.kt @@ -0,0 +1,196 @@ +package org.koitharu.kotatsu.ui.main.list + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.sheet_list.* +import moxy.MvpDelegate +import org.koin.android.ext.android.inject +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaFilter +import org.koitharu.kotatsu.core.model.MangaTag +import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.ui.common.BaseBottomSheet +import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener +import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener +import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.ui.details.MangaDetailsActivity +import org.koitharu.kotatsu.utils.UiUtils +import org.koitharu.kotatsu.utils.ext.* + +abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), MangaListView, + PaginationScrollListener.Callback, OnRecyclerItemClickListener, + SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener { + + private val settings by inject() + + private var adapter: MangaListAdapter? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + adapter = MangaListAdapter(this) + initListMode(settings.listMode) + recyclerView.adapter = adapter + recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) + settings.subscribe(this) + toolbar.inflateMenu(R.menu.opt_list_sheet) + toolbar.setOnMenuItemClickListener(this) + toolbar.setNavigationOnClickListener { + dismiss() + } + if (dialog !is BottomSheetDialog) { + toolbar.isVisible = true + textView_title.isVisible = false + appbar.elevation = resources.getDimension(R.dimen.elevation_large) + } + } + + override fun onDestroyView() { + settings.unsubscribe(this) + adapter = null + super.onDestroyView() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) { + onRequestMoreItems(0) + } + } + + protected fun setTitle(title: CharSequence) { + toolbar.title = title + textView_title.text = title + } + + protected fun setSubtitle(subtitle: CharSequence) { + toolbar.subtitle = subtitle + } + + override fun onCreateDialog(savedInstanceState: Bundle?) = + super.onCreateDialog(savedInstanceState).also { + val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also + behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + private val elevation = resources.getDimension(R.dimen.elevation_large) + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_EXPANDED) { + toolbar.isVisible = true + textView_title.isVisible = false + appbar.elevation = elevation + } else { + toolbar.isVisible = false + textView_title.isVisible = true + appbar.elevation = 0f + } + } + }) + + } + + override fun onMenuItemClick(item: MenuItem) = when (item.itemId) { + R.id.action_list_mode -> { + ListModeSelectDialog.show(childFragmentManager) + true + } + else -> false + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + when (key) { + getString(R.string.key_list_mode) -> initListMode(settings.listMode) + getString(R.string.key_grid_size) -> UiUtils.SpanCountResolver.update(recyclerView) + } + } + + override fun onItemClick(item: Manga, position: Int, view: View) { + startActivity(MangaDetailsActivity.newIntent(context ?: return, item)) + } + + override fun onListChanged(list: List) { + adapter?.replaceData(list) + textView_holder.isVisible = list.isEmpty() + recyclerView.callOnScrollListeners() + } + + override fun onListAppended(list: List) { + adapter?.appendData(list) + if (list.isNotEmpty()) { + textView_holder.isVisible = false + } + recyclerView.callOnScrollListeners() + } + + override fun onListError(e: Throwable) { + Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show() + } + + override fun onInitFilter( + sortOrders: List, + tags: List, + currentFilter: MangaFilter? + ) = Unit + + override fun onItemRemoved(item: Manga) { + adapter?.let { + it.removeItem(item) + textView_holder.isGone = it.hasItems + } + } + + override fun onError(e: Throwable) { + Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show() + } + + override fun onLoadingStateChanged(isLoading: Boolean) { + progressBar.isVisible = isLoading && !recyclerView.hasItems + if (isLoading) { + textView_holder.isVisible = false + } + } + + private fun initListMode(mode: ListMode) { + val ctx = context ?: return + val position = recyclerView.firstItem + recyclerView.adapter = null + recyclerView.layoutManager = null + recyclerView.clearItemDecorations() + recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) + adapter?.listMode = mode + recyclerView.layoutManager = when (mode) { + ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)) + else -> LinearLayoutManager(ctx) + } + recyclerView.adapter = adapter + recyclerView.addItemDecoration( + when (mode) { + ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) + ListMode.DETAILED_LIST, + ListMode.GRID -> SpacingItemDecoration( + resources.getDimensionPixelOffset(R.dimen.grid_spacing) + ) + } + ) + if (mode == ListMode.GRID) { + recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver) + } + adapter?.notifyDataSetChanged() + recyclerView.firstItem = position + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/search/MangaSearchSheet.kt b/app/src/main/java/org/koitharu/kotatsu/ui/search/MangaSearchSheet.kt new file mode 100644 index 000000000..eb24e8bfc --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/search/MangaSearchSheet.kt @@ -0,0 +1,50 @@ +package org.koitharu.kotatsu.ui.search + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.FragmentManager +import moxy.ktx.moxyPresenter +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.ui.main.list.MangaListSheet +import org.koitharu.kotatsu.utils.ext.withArgs + +class MangaSearchSheet : MangaListSheet() { + + private val presenter by moxyPresenter(factory = ::SearchPresenter) + + private lateinit var source: MangaSource + private lateinit var query: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + source = requireArguments().getParcelable(ARG_SOURCE)!! + query = requireArguments().getString(ARG_QUERY).orEmpty() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setTitle(query) + setSubtitle(getString(R.string.search_results_on_s, source.title)) + } + + override fun onRequestMoreItems(offset: Int) { + presenter.loadList(source, query, offset) + } + + companion object { + + private const val ARG_SOURCE = "source" + private const val ARG_QUERY = "query" + + private const val TAG = "MangaSearchSheet" + + fun show(fm: FragmentManager, source: MangaSource, query: String) { + MangaSearchSheet().withArgs(2) { + putParcelable(ARG_SOURCE, source) + putString(ARG_QUERY, query) + }.show(fm, TAG) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/sheet_list.xml b/app/src/main/res/layout/sheet_list.xml new file mode 100644 index 000000000..a9fd25a89 --- /dev/null +++ b/app/src/main/res/layout/sheet_list.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_list_sheet.xml b/app/src/main/res/menu/opt_list_sheet.xml new file mode 100644 index 000000000..421f8a2c5 --- /dev/null +++ b/app/src/main/res/menu/opt_list_sheet.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file