diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt new file mode 100644 index 000000000..012ded842 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.core.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class MangaFilter( + val sortOrder: SortOrder, + val tag: MangaTag? +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/SortOrder.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/SortOrder.kt index c111e51d9..99ee3b835 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/SortOrder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/SortOrder.kt @@ -1,5 +1,12 @@ package org.koitharu.kotatsu.core.model -enum class SortOrder { - ALPHABETICAL, POPULARITY, UPDATED, NEWEST, RATING +import androidx.annotation.StringRes +import org.koitharu.kotatsu.R + +enum class SortOrder(@StringRes val titleRes: Int) { + UPDATED(R.string.updated), + POPULARITY(R.string.popular), + RATING(R.string.by_rating), + NEWEST(R.string.newest), + ALPHABETICAL(R.string.by_name) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt index cf0173b8e..44849b57b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt @@ -15,8 +15,9 @@ abstract class GroupleRepository( protected abstract val domain: String override val sortOrders = setOf( - SortOrder.ALPHABETICAL, SortOrder.POPULARITY, - SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.RATING + SortOrder.UPDATED, SortOrder.POPULARITY, + SortOrder.NEWEST, SortOrder.RATING + //FIXME SortOrder.ALPHABETICAL ) override suspend fun getList( @@ -26,9 +27,16 @@ abstract class GroupleRepository( tag: MangaTag? ): List { val doc = when { - !query.isNullOrEmpty() -> loaderContext.post("https://$domain/search", mapOf("q" to query)) + !query.isNullOrEmpty() -> loaderContext.post( + "https://$domain/search", + mapOf("q" to query) + ) tag == null -> loaderContext.get("https://$domain/list?sortType=${getSortKey(sortOrder)}&offset=$offset") - else -> loaderContext.get( "https://$domain/list/genre/${tag.key}?sortType=${getSortKey(sortOrder)}&offset=$offset") + else -> loaderContext.get( + "https://$domain/list/genre/${tag.key}?sortType=${getSortKey( + sortOrder + )}&offset=$offset" + ) }.parseHtml() val root = doc.body().getElementById("mangaBox") ?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root") @@ -128,19 +136,20 @@ abstract class GroupleRepository( .selectFirst("table.table") return root.select("a.element-link").map { a -> MangaTag( - title = a.text(), + title = a.text().capitalize(), key = a.attr("href").substringAfterLast('/'), source = source ) }.toSet() } - private fun getSortKey(sortOrder: SortOrder?) = when (sortOrder) { - SortOrder.ALPHABETICAL -> "name" - SortOrder.POPULARITY -> "rate" - SortOrder.UPDATED -> "updated" - SortOrder.NEWEST -> "created" - SortOrder.RATING -> "votes" - null -> "updated" - } + private fun getSortKey(sortOrder: SortOrder?) = + when (sortOrder ?: sortOrders.minBy { it.ordinal }) { + SortOrder.ALPHABETICAL -> "name" + SortOrder.POPULARITY -> "rate" + SortOrder.UPDATED -> "updated" + SortOrder.NEWEST -> "created" + SortOrder.RATING -> "votes" + null -> "updated" + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseActivity.kt index f6cb18b76..eb0230f18 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseActivity.kt @@ -1,10 +1,12 @@ package org.koitharu.kotatsu.ui.common +import android.view.KeyEvent import android.view.MenuItem import android.view.View import androidx.appcompat.widget.Toolbar import moxy.MvpAppCompatActivity import org.koin.core.KoinComponent +import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent { @@ -27,4 +29,13 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent { onBackPressed() true } else super.onOptionsItemSelected(item) + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + //TODO remove. Just for testing + if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + recreate() + return true + } + return super.onKeyDown(keyCode, event) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseBottomSheet.kt new file mode 100644 index 000000000..32a65e01b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseBottomSheet.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.ui.common + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import moxy.MvpBottomSheetDialogFragment + +abstract class BaseBottomSheet(@LayoutRes private val layoutResId: Int) : MvpBottomSheetDialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(layoutResId, container, false) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseFragment.kt index 34c3be382..d7711a2e3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseFragment.kt @@ -1,26 +1,19 @@ package org.koitharu.kotatsu.ui.common import android.content.Context -import android.content.SharedPreferences import android.os.Parcelable import androidx.annotation.LayoutRes import moxy.MvpAppCompatFragment -import org.koin.android.ext.android.inject -import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.utils.delegates.ParcelableArgumentDelegate import org.koitharu.kotatsu.utils.delegates.StringArgumentDelegate abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : - MvpAppCompatFragment(contentLayoutId), SharedPreferences.OnSharedPreferenceChangeListener { - - protected val settings by inject() + MvpAppCompatFragment(contentLayoutId) { fun stringArg(name: String) = StringArgumentDelegate(name) fun arg(name: String) = ParcelableArgumentDelegate(name) - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = Unit - open fun getTitle(): CharSequence? = null override fun onAttach(context: Context) { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BaseViewHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BaseViewHolder.kt index 2f9a25f13..e6cd0421b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BaseViewHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BaseViewHolder.kt @@ -16,7 +16,7 @@ abstract class BaseViewHolder protected constructor(view: View) : override val containerView: View? get() = itemView - protected var boundData: T? = null + var boundData: T? = null private set val context get() = itemView.context!! diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/ItemTypeDividerDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/ItemTypeDividerDecoration.kt new file mode 100644 index 000000000..d4920f9fe --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/ItemTypeDividerDecoration.kt @@ -0,0 +1,58 @@ +package org.koitharu.kotatsu.ui.common.list.decor + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.view.View +import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.utils.ext.getThemeDrawable +import kotlin.math.roundToInt + +class ItemTypeDividerDecoration(context: Context) : RecyclerView.ItemDecoration() { + + private val divider = context.getThemeDrawable(android.R.attr.listDivider) + private val bounds = Rect() + + override fun getItemOffsets( + outRect: Rect, view: View, + parent: RecyclerView, state: RecyclerView.State + ) { + outRect.set(0, divider?.intrinsicHeight ?: 0, 0, 0) + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) { + if (parent.layoutManager == null || divider == null) { + return + } + val adapter = parent.adapter ?: return + canvas.save() + val left: Int + val right: Int + if (parent.clipToPadding) { + left = parent.paddingLeft + right = parent.width - parent.paddingRight + canvas.clipRect( + left, parent.paddingTop, right, + parent.height - parent.paddingBottom + ) + } else { + left = 0 + right = parent.width + } + + var lastItemType = -1 + for (child in parent.children) { + val itemType = adapter.getItemViewType(parent.getChildAdapterPosition(child)) + if (lastItemType != -1 && itemType != lastItemType) { + parent.getDecoratedBoundsWithMargins(child, bounds) + val top: Int = bounds.top + child.translationY.roundToInt() + val bottom: Int = top + divider.intrinsicHeight + divider.setBounds(left, top, right, bottom) + divider.draw(canvas) + } + lastItemType = itemType + } + canvas.restore() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/SectionItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/SectionItemDecoration.kt new file mode 100644 index 000000000..7b5f383f6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/SectionItemDecoration.kt @@ -0,0 +1,96 @@ +package org.koitharu.kotatsu.ui.common.list.decor + +import android.graphics.Canvas +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.utils.ext.inflate +import kotlin.math.max + +/** + * https://github.com/paetztm/recycler_view_headers + */ +class SectionItemDecoration( + private val isSticky: Boolean, + private val callback: Callback +) : RecyclerView.ItemDecoration() { + + private var headerView: TextView? = null + private var headerOffset: Int = 0 + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + if (headerOffset == 0) { + headerOffset = parent.resources.getDimensionPixelSize(R.dimen.header_height) + } + val pos = parent.getChildAdapterPosition(view) + outRect.set(0, if (callback.isSection(pos)) headerOffset else 0, 0, 0) + } + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(c, parent, state) + val textView = headerView ?: parent.inflate(R.layout.item_header).also { + headerView = it + } + fixLayoutSize(textView, parent) + + for (child in parent.children) { + val pos = parent.getChildAdapterPosition(child) + if (callback.isSection(pos)) { + textView.text = callback.getSectionTitle(pos) ?: continue + c.save() + if (isSticky) { + c.translate( + 0f, + max(0f, (child.top - textView.height).toFloat()) + ) + } else { + c.translate( + 0f, + (child.top - textView.height).toFloat() + ) + } + textView.draw(c) + c.restore() + } + } + } + + /** + * Measures the header view to make sure its size is greater than 0 and will be drawn + * https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations + */ + private fun fixLayoutSize(view: View, parent: ViewGroup) { + val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) + val heightSpec = + View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) + + val childWidth = ViewGroup.getChildMeasureSpec( + widthSpec, + parent.paddingLeft + parent.paddingRight, + view.layoutParams.width + ) + val childHeight = ViewGroup.getChildMeasureSpec( + heightSpec, + parent.paddingTop + parent.paddingBottom, + view.layoutParams.height + ) + view.measure(childWidth, childHeight) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + } + + interface Callback { + + fun isSection(position: Int): Boolean + + fun getSectionTitle(position: Int): CharSequence? + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/SpacingItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/SpacingItemDecoration.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/ui/common/list/SpacingItemDecoration.kt rename to app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/SpacingItemDecoration.kt index 6b3f59e45..3305bfde3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/SpacingItemDecoration.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/decor/SpacingItemDecoration.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.ui.common.list +package org.koitharu.kotatsu.ui.common.list.decor import android.graphics.Rect import android.view.View diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt index 66b901bb2..c626bd3d1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt @@ -33,7 +33,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList navigationView.setNavigationItemSelectedListener(this) - if (!supportFragmentManager.isStateSaved) { + if (supportFragmentManager.findFragmentById(R.id.container) == null) { navigationView.setCheckedItem(R.id.nav_local_storage) setPrimaryFragment(LocalListFragment.newInstance()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListFragment.kt index eb8a7d1c6..60a055176 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListFragment.kt @@ -6,28 +6,41 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.annotation.CallSuper +import androidx.core.view.GravityCompat import androidx.core.view.isVisible +import androidx.drawerlayout.widget.DrawerLayout 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.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_list.* +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.BaseFragment import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener -import org.koitharu.kotatsu.ui.common.list.SpacingItemDecoration +import org.koitharu.kotatsu.ui.common.list.decor.ItemTypeDividerDecoration +import org.koitharu.kotatsu.ui.common.list.decor.SectionItemDecoration +import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.ui.details.MangaDetailsActivity -import org.koitharu.kotatsu.utils.ext.clearItemDecorations -import org.koitharu.kotatsu.utils.ext.firstItem -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.hasItems +import org.koitharu.kotatsu.ui.main.list.filter.FilterAdapter +import org.koitharu.kotatsu.ui.main.list.filter.OnFilterChangedListener +import org.koitharu.kotatsu.utils.ext.* -abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView, - PaginationScrollListener.Callback, OnRecyclerItemClickListener { +abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView, + PaginationScrollListener.Callback, OnRecyclerItemClickListener, + SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener, + SectionItemDecoration.Callback { + + private val settings by inject() private lateinit var adapter: MangaListAdapter @@ -38,6 +51,7 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), Man override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) adapter = MangaListAdapter(this) initListMode(settings.listMode) recyclerView.adapter = adapter @@ -45,6 +59,8 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), Man swipeRefreshLayout.setOnRefreshListener { onRequestMoreItems(0) } + recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context)) + recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this)) settings.subscribe(this) } @@ -55,7 +71,7 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), Man override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - if (!recyclerView.hasItems) { + if (savedInstanceState?.containsKey("MoxyDelegateBundle") != true) { onRequestMoreItems(0) } } @@ -65,14 +81,24 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), Man super.onCreateOptionsMenu(menu, inflater) } - override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) { + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { R.id.action_list_mode -> { ListModeSelectDialog.show(childFragmentManager) true } + R.id.action_filter -> { + drawer.toggleDrawer(GravityCompat.END) + true + } else -> super.onOptionsItemSelected(item) } + override fun onPrepareOptionsMenu(menu: Menu) { + menu.findItem(R.id.action_filter).isVisible = + drawer.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED + super.onPrepareOptionsMenu(menu) + } + override fun onItemClick(item: Manga, position: Int, view: View) { startActivity(MangaDetailsActivity.newIntent(context ?: return, item)) } @@ -93,10 +119,16 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), Man override fun onError(e: Exception) { if (recyclerView.hasItems) { - Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show() + Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT) + .show() } else { textView_holder.text = e.getDisplayMessage(resources) - textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, R.drawable.ic_error_large, 0, 0) + textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds( + 0, + R.drawable.ic_error_large, + 0, + 0 + ) layout_holder.isVisible = true } } @@ -112,11 +144,32 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), Man } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - when(key) { + when (key) { getString(R.string.key_list_mode) -> initListMode(settings.listMode) } } + override fun onInitFilter( + sortOrders: List, + tags: List, + currentFilter: MangaFilter? + ) { + recyclerView_filter.adapter = FilterAdapter(sortOrders, tags, currentFilter, this) + drawer.setDrawerLockMode( + if (sortOrders.isEmpty() && tags.isEmpty()) { + DrawerLayout.LOCK_MODE_LOCKED_CLOSED + } else { + DrawerLayout.LOCK_MODE_UNLOCKED + } + ) + activity?.invalidateOptionsMenu() + } + + @CallSuper + override fun onFilterChanged(filter: MangaFilter) { + drawer.closeDrawers() + } + protected open fun setUpEmptyListHolder() { textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null) textView_holder.setText(R.string.nothing_found) @@ -129,17 +182,35 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), Man recyclerView.layoutManager = null recyclerView.clearItemDecorations() adapter.listMode = mode - recyclerView.layoutManager = when(mode) { + recyclerView.layoutManager = when (mode) { ListMode.GRID -> GridLayoutManager(ctx, 3) 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)) - }) + recyclerView.addItemDecoration( + when (mode) { + ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) + ListMode.DETAILED_LIST, + ListMode.GRID -> SpacingItemDecoration( + resources.getDimensionPixelOffset(R.dimen.grid_spacing) + ) + } + ) adapter.notifyDataSetChanged() recyclerView.firstItem = position } + + override fun isSection(position: Int): Boolean { + return position == 0 || recyclerView_filter.adapter?.run { + getItemViewType(position) != getItemViewType(position - 1) + } ?: false + } + + override fun getSectionTitle(position: Int): CharSequence? { + return when (recyclerView_filter.adapter?.getItemViewType(position)) { + FilterAdapter.VIEW_TYPE_SORT -> getString(R.string.sort_order) + FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genre) + else -> null + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListHolder.kt index 2f8c048ac..7c5f91d13 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListHolder.kt @@ -10,14 +10,15 @@ import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.utils.ext.textAndVisible -class MangaListHolder(parent: ViewGroup) : BaseViewHolder(parent, R.layout.item_manga_list) { +class MangaListHolder(parent: ViewGroup) : + BaseViewHolder(parent, R.layout.item_manga_list) { private var coverRequest: RequestDisposable? = null override fun onBind(data: Manga, extra: MangaHistory?) { coverRequest?.dispose() textView_title.text = data.title - textView_subtitle.textAndVisible = data.altTitle + textView_subtitle.textAndVisible = data.tags.joinToString(", ") { it.title } coverRequest = imageView_cover.load(data.coverUrl) { crossfade(true) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListView.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListView.kt index 939fafc61..6cf737756 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/MangaListView.kt @@ -3,6 +3,9 @@ package org.koitharu.kotatsu.ui.main.list import moxy.MvpView import moxy.viewstate.strategy.* 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 interface MangaListView : MvpView { @@ -17,4 +20,7 @@ interface MangaListView : MvpView { @StateStrategyType(OneExecutionStateStrategy::class) fun onError(e: Exception) + + @StateStrategyType(AddToEndSingleStrategy::class) + fun onInitFilter(sortOrders: List, tags: List, currentFilter: MangaFilter?) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoriesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoriesAdapter.kt index bc1307d9f..cdc26408f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoriesAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoriesAdapter.kt @@ -7,7 +7,6 @@ import androidx.core.util.set import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter import org.koitharu.kotatsu.ui.common.list.BaseViewHolder -import org.koitharu.kotatsu.utils.ext.disableFor class CategoriesAdapter(private val listener: OnCategoryCheckListener) : BaseRecyclerAdapter() { @@ -37,7 +36,6 @@ class CategoriesAdapter(private val listener: OnCategoryCheckListener) : holder.itemView.setOnClickListener { if (it !is Checkable) return@setOnClickListener it.toggle() - it.disableFor(200) if (it.isChecked) { listener.onCategoryChecked(holder.requireData()) } else { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoryHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoryHolder.kt index a9ab85a8b..9716b880a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoryHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/CategoryHolder.kt @@ -1,13 +1,13 @@ package org.koitharu.kotatsu.ui.main.list.favourites.categories import android.view.ViewGroup -import kotlinx.android.synthetic.main.item_caegory_checkable.* +import kotlinx.android.synthetic.main.item_category_checkable.* import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.ui.common.list.BaseViewHolder class CategoryHolder(parent: ViewGroup) : - BaseViewHolder(parent, R.layout.item_caegory_checkable) { + BaseViewHolder(parent, R.layout.item_category_checkable) { override fun onBind(data: FavouriteCategory, extra: Boolean) { checkedTextView.text = data.title diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/FavouriteCategoriesDialog.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/FavouriteCategoriesDialog.kt index d3f741957..947ea76ed 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/FavouriteCategoriesDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/favourites/categories/FavouriteCategoriesDialog.kt @@ -4,19 +4,18 @@ import android.os.Bundle import android.text.InputType import android.view.View import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentManager import kotlinx.android.synthetic.main.dialog_favorite_categories.* import moxy.ktx.moxyPresenter import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.ui.common.AlertDialogFragment +import org.koitharu.kotatsu.ui.common.BaseBottomSheet import org.koitharu.kotatsu.ui.common.TextInputDialog import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.withArgs -class FavouriteCategoriesDialog() : AlertDialogFragment(R.layout.dialog_favorite_categories), +class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_categories), FavouriteCategoriesView, OnCategoryCheckListener { @@ -26,10 +25,6 @@ class FavouriteCategoriesDialog() : AlertDialogFragment(R.layout.dialog_favorite private var adapter: CategoriesAdapter? = null - override fun onBuildDialog(builder: AlertDialog.Builder) { - builder.setTitle(R.string.add_to_favourites) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) adapter = CategoriesAdapter(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterAdapter.kt new file mode 100644 index 000000000..0e543b8df --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterAdapter.kt @@ -0,0 +1,93 @@ +package org.koitharu.kotatsu.ui.main.list.filter + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +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.ui.common.list.BaseViewHolder +import java.util.* +import kotlin.collections.ArrayList + +class FilterAdapter( + sortOrders: List = emptyList(), + tags: List = emptyList(), + state: MangaFilter?, + private val listener: OnFilterChangedListener +) : RecyclerView.Adapter>() { + + private val sortOrders = ArrayList(sortOrders) + private val tags = ArrayList(Collections.singletonList(null) + tags) + + private var currentState = state ?: MangaFilter(sortOrders.first(), null) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { + VIEW_TYPE_SORT -> FilterSortHolder(parent).apply { + itemView.setOnClickListener { + setCheckedSort(requireData()) + } + } + VIEW_TYPE_TAG -> FilterTagHolder(parent).apply { + itemView.setOnClickListener { + setCheckedTag(boundData) + } + } + else -> throw IllegalArgumentException("Unknown viewType $viewType") + } + + override fun getItemCount() = sortOrders.size + tags.size + + override fun onBindViewHolder(holder: BaseViewHolder<*, Boolean>, position: Int) { + when (holder) { + is FilterSortHolder -> { + val item = sortOrders[position] + holder.bind(item, item == currentState.sortOrder) + } + is FilterTagHolder -> { + val item = tags[position - sortOrders.size] + holder.bind(item, item == currentState.tag) + } + } + } + + override fun getItemViewType(position: Int) = when (position) { + in sortOrders.indices -> VIEW_TYPE_SORT + else -> VIEW_TYPE_TAG + } + + fun setCheckedTag(tag: MangaTag?) { + if (tag != currentState.tag) { + val oldItemPos = tags.indexOf(currentState.tag) + val newItemPos = tags.indexOf(tag) + currentState = currentState.copy(tag = tag) + if (oldItemPos in tags.indices) { + notifyItemChanged(sortOrders.size + oldItemPos) + } + if (newItemPos in tags.indices) { + notifyItemChanged(sortOrders.size + newItemPos) + } + listener.onFilterChanged(currentState) + } + } + + fun setCheckedSort(sort: SortOrder) { + if (sort != currentState.sortOrder) { + val oldItemPos = sortOrders.indexOf(currentState.sortOrder) + val newItemPos = sortOrders.indexOf(sort) + currentState = currentState.copy(sortOrder = sort) + if (oldItemPos in sortOrders.indices) { + notifyItemChanged(oldItemPos) + } + if (newItemPos in sortOrders.indices) { + notifyItemChanged(newItemPos) + } + listener.onFilterChanged(currentState) + } + } + + companion object { + + const val VIEW_TYPE_SORT = 0 + const val VIEW_TYPE_TAG = 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterSortHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterSortHolder.kt new file mode 100644 index 000000000..2f66ffbda --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterSortHolder.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.ui.main.list.filter + +import android.view.ViewGroup +import kotlinx.android.synthetic.main.item_checkable_single.* +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.ui.common.list.BaseViewHolder + +class FilterSortHolder(parent: ViewGroup) : + BaseViewHolder(parent, R.layout.item_checkable_single) { + + override fun onBind(data: SortOrder, extra: Boolean) { + radio.setText(data.titleRes) + radio.isChecked = extra + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterTagHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterTagHolder.kt new file mode 100644 index 000000000..e44150478 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/FilterTagHolder.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.ui.main.list.filter + +import android.view.ViewGroup +import kotlinx.android.synthetic.main.item_checkable_single.* +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.MangaTag +import org.koitharu.kotatsu.ui.common.list.BaseViewHolder + +class FilterTagHolder(parent: ViewGroup) : + BaseViewHolder(parent, R.layout.item_checkable_single) { + + override fun onBind(data: MangaTag?, extra: Boolean) { + radio.text = data?.title ?: context.getString(R.string.all) + radio.isChecked = extra + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/OnFilterChangedListener.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/OnFilterChangedListener.kt new file mode 100644 index 000000000..077114921 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/filter/OnFilterChangedListener.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.ui.main.list.filter + +import org.koitharu.kotatsu.core.model.MangaFilter + +interface OnFilterChangedListener { + + fun onFilterChanged(filter: MangaFilter) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListFragment.kt index 68887dfad..b07654703 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListFragment.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.ui.main.list.remote import moxy.ktx.moxyPresenter +import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.utils.ext.withArgs @@ -19,6 +20,11 @@ class RemoteListFragment : MangaListFragment() { return source.title } + override fun onFilterChanged(filter: MangaFilter) { + presenter.applyFilter(source, filter) + super.onFilterChanged(filter) + } + companion object { private const val ARG_SOURCE = "provider" diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListPresenter.kt index 844e6af6b..2702cfda1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListPresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/remote/RemoteListPresenter.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import moxy.InjectViewState import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.ui.common.BasePresenter @@ -13,13 +14,19 @@ import org.koitharu.kotatsu.ui.main.list.MangaListView @InjectViewState class RemoteListPresenter : BasePresenter>() { + private var isFilterInitialized = false + private var filter: MangaFilter? = null + fun loadList(source: MangaSource, offset: Int) { launch { viewState.onLoadingChanged(true) try { val list = withContext(Dispatchers.IO) { - MangaProviderFactory.create(source) - .getList(offset) + MangaProviderFactory.create(source).getList( + offset = offset, + sortOrder = filter?.sortOrder, + tag = filter?.tag + ) } if (offset == 0) { viewState.onListChanged(list) @@ -35,5 +42,32 @@ class RemoteListPresenter : BasePresenter>() { viewState.onLoadingChanged(false) } } + if (!isFilterInitialized) { + loadFilter(source) + } + } + + fun applyFilter(source: MangaSource, filter: MangaFilter) { + this.filter = filter + viewState.onListChanged(emptyList()) + loadList(source, 0) + } + + private fun loadFilter(source: MangaSource) { + isFilterInitialized = true + launch { + try { + val (sorts, tags) = withContext(Dispatchers.IO) { + val repo = MangaProviderFactory.create(source) + repo.sortOrders.sortedBy { it.ordinal } to repo.getTags().sortedBy { it.title } + } + viewState.onInitFilter(sorts, tags, filter) + } catch (e: Exception) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + isFilterInitialized = false + } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index d7de3f258..04b75a4fe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -13,6 +13,7 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.postDelayed +import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -131,4 +132,12 @@ fun View.hasGlobalPoint(x: Int, y: Int): Boolean { val rect = Rect() getGlobalVisibleRect(rect) return rect.contains(x, y) +} + +fun DrawerLayout.toggleDrawer(gravity: Int) { + if (isDrawerOpen(gravity)) { + closeDrawer(gravity) + } else { + openDrawer(gravity) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_favorite_categories.xml b/app/src/main/res/layout/dialog_favorite_categories.xml index 96cfe5d1b..3c0668c4c 100644 --- a/app/src/main/res/layout/dialog_favorite_categories.xml +++ b/app/src/main/res/layout/dialog_favorite_categories.xml @@ -5,8 +5,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:paddingTop="12dp"> + android:orientation="vertical"> + + + tools:listitem="@layout/item_category_checkable" /> - + android:layout_height="match_parent" + tools:openDrawer="end"> - - + android:layout_height="match_parent"> - + - + - + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" + android:gravity="center_horizontal" + android:orientation="vertical"> - + - + - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_caegory_checkable.xml b/app/src/main/res/layout/item_category_checkable.xml similarity index 100% rename from app/src/main/res/layout/item_caegory_checkable.xml rename to app/src/main/res/layout/item_category_checkable.xml diff --git a/app/src/main/res/layout/item_checkable_single.xml b/app/src/main/res/layout/item_checkable_single.xml new file mode 100644 index 000000000..af15894cd --- /dev/null +++ b/app/src/main/res/layout/item_checkable_single.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml new file mode 100644 index 000000000..3598ed294 --- /dev/null +++ b/app/src/main/res/layout/item_header.xml @@ -0,0 +1,15 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml index 6311ffb0c..91713e10d 100644 --- a/app/src/main/res/layout/item_manga_grid.xml +++ b/app/src/main/res/layout/item_manga_grid.xml @@ -1,9 +1,15 @@ + android:layout_height="wrap_content" + app:cardBackgroundColor="?android:windowBackground" + app:cardElevation="0dp" + app:cardMaxElevation="0dp" + app:strokeColor="?android:textColorPrimary" + app:strokeWidth="1px"> - + android:layout_height="match_parent" + android:orientation="vertical" /> + app:cardElevation="0dp" + app:cardMaxElevation="0dp" + app:strokeColor="?android:textColorPrimary" + app:strokeWidth="1px"> + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 9184a28f1..dd24c0ab2 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,4 +4,6 @@ 84dp 120dp 46dp + 120dp + 42dp \ 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 145c389c7..2e7498103 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,4 +46,13 @@ Save this chapter and next Save this chapter No saved manga + By name + Popular + Updated + Newest + By rating + All + Sort order + Genre + Filter \ No newline at end of file