This commit is contained in:
Koitharu
2020-02-15 18:20:06 +02:00
parent 315aea8b5c
commit 2309d2465b
35 changed files with 641 additions and 106 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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<Manga> {
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"
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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<AppSettings>()
MvpAppCompatFragment(contentLayoutId) {
fun stringArg(name: String) = StringArgumentDelegate(name)
fun <T : Parcelable> arg(name: String) = ParcelableArgumentDelegate<T>(name)
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = Unit
open fun getTitle(): CharSequence? = null
override fun onAttach(context: Context) {

View File

@@ -16,7 +16,7 @@ abstract class BaseViewHolder<T, E> 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!!

View File

@@ -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()
}
}

View File

@@ -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<TextView>(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?
}
}

View File

@@ -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

View File

@@ -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())
}

View File

@@ -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 <E> : BaseFragment(R.layout.fragment_list), MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga> {
abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener,
SectionItemDecoration.Callback {
private val settings by inject<AppSettings>()
private lateinit var adapter: MangaListAdapter
@@ -38,6 +51,7 @@ abstract class MangaListFragment <E> : 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 <E> : 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 <E> : 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 <E> : 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 <E> : 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 <E> : 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<SortOrder>,
tags: List<MangaTag>,
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 <E> : 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
}
}
}

View File

@@ -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<Manga, MangaHistory?>(parent, R.layout.item_manga_list) {
class MangaListHolder(parent: ViewGroup) :
BaseViewHolder<Manga, MangaHistory?>(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)
}

View File

@@ -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<E> : MvpView {
@@ -17,4 +20,7 @@ interface MangaListView<E> : MvpView {
@StateStrategyType(OneExecutionStateStrategy::class)
fun onError(e: Exception)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onInitFilter(sortOrders: List<SortOrder>, tags: List<MangaTag>, currentFilter: MangaFilter?)
}

View File

@@ -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<FavouriteCategory, Boolean>() {
@@ -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 {

View File

@@ -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<FavouriteCategory, Boolean>(parent, R.layout.item_caegory_checkable) {
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_category_checkable) {
override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title

View File

@@ -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)

View File

@@ -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<SortOrder> = emptyList(),
tags: List<MangaTag> = emptyList(),
state: MangaFilter?,
private val listener: OnFilterChangedListener
) : RecyclerView.Adapter<BaseViewHolder<*, Boolean>>() {
private val sortOrders = ArrayList<SortOrder>(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
}
}

View File

@@ -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<SortOrder, Boolean>(parent, R.layout.item_checkable_single) {
override fun onBind(data: SortOrder, extra: Boolean) {
radio.setText(data.titleRes)
radio.isChecked = extra
}
}

View File

@@ -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<MangaTag?, Boolean>(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
}
}

View File

@@ -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)
}

View File

@@ -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<Unit>() {
return source.title
}
override fun onFilterChanged(filter: MangaFilter) {
presenter.applyFilter(source, filter)
super.onFilterChanged(filter)
}
companion object {
private const val ARG_SOURCE = "provider"

View File

@@ -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<MangaListView<Unit>>() {
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<MangaListView<Unit>>() {
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
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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">
<TextView
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:padding="16dp"
android:textColor="?android:textColorSecondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_to_favourites" />
<View
android:layout_width="match_parent"
@@ -20,7 +27,7 @@
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_caegory_checkable" />
tools:listitem="@layout/item_category_checkable" />
<View
android:layout_width="match_parent"

View File

@@ -1,53 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:openDrawer="end">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
android:layout_height="match_parent">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_manga_list" />
<LinearLayout
android:id="@+id/layout_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:orientation="vertical">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/textView_holder"
<LinearLayout
android:id="@+id/layout_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="?android:textColorSecondary"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="@tools:sample/lorem[3]" />
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:gravity="center_horizontal"
android:orientation="vertical">
</LinearLayout>
<TextView
android:id="@+id/textView_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="?android:textColorSecondary"
android:textAppearance="?android:textAppearanceMedium"
tools:text="@tools:sample/lorem[3]" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</LinearLayout>
</FrameLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_filter"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:background="?android:windowBackground"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_category_checkable" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/radio"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground"
android:drawableStart="?android:listChoiceIndicatorSingle"
android:drawablePadding="12dp"
android:gravity="center_vertical|start"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
tools:text="@tools:sample/full_names" />

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/header_height"
android:background="?android:windowBackground"
android:gravity="center_vertical|start"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="?android:textColorSecondary"
android:textStyle="bold"
tools:text="@tools:sample/lorem[2]" />

View File

@@ -1,9 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:cardBackgroundColor="?android:windowBackground"
app:cardElevation="0dp"
app:cardMaxElevation="0dp"
app:strokeColor="?android:textColorPrimary"
app:strokeWidth="1px">
<LinearLayout
android:layout_width="wrap_content"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/manga_list_item_height"
android:background="?selectableItemBackground"
@@ -8,8 +9,8 @@
<org.koitharu.kotatsu.ui.common.widgets.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="wrap_content"
android:orientation="vertical"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:orientation="vertical" />
<LinearLayout
android:layout_width="match_parent"

View File

@@ -1,14 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardElevation="0dp"
app:strokeColor="?android:textColorPrimary"
app:strokeWidth="1px"
android:layout_height="@dimen/manga_list_details_item_height"
app:cardBackgroundColor="?android:windowBackground"
android:layout_height="@dimen/manga_list_details_item_height">
app:cardElevation="0dp"
app:cardMaxElevation="0dp"
app:strokeColor="?android:textColorPrimary"
app:strokeWidth="1px">
<RelativeLayout
android:layout_width="match_parent"

View File

@@ -5,7 +5,13 @@
<item
android:id="@+id/action_list_mode"
android:title="@string/list_mode"
android:orderInCategory="20"
android:title="@string/list_mode"
app:showAsAction="never" />
<item
android:id="@+id/action_filter"
android:orderInCategory="30"
android:title="@string/filter"
app:showAsAction="never" />
</menu>

View File

@@ -4,4 +4,6 @@
<dimen name="manga_list_item_height">84dp</dimen>
<dimen name="manga_list_details_item_height">120dp</dimen>
<dimen name="chapter_list_item_height">46dp</dimen>
<dimen name="preferred_grid_width">120dp</dimen>
<dimen name="header_height">42dp</dimen>
</resources>

View File

@@ -46,4 +46,13 @@
<string name="save_this_chapter_and_next">Save this chapter and next</string>
<string name="save_this_chapter">Save this chapter</string>
<string name="no_saved_manga">No saved manga</string>
<string name="by_name">By name</string>
<string name="popular">Popular</string>
<string name="updated">Updated</string>
<string name="newest">Newest</string>
<string name="by_rating">By rating</string>
<string name="all">All</string>
<string name="sort_order">Sort order</string>
<string name="genre">Genre</string>
<string name="filter">Filter</string>
</resources>