pull/1/head
Koitharu 6 years ago
parent 315aea8b5c
commit 2309d2465b

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

@ -1,5 +1,12 @@
package org.koitharu.kotatsu.core.model package org.koitharu.kotatsu.core.model
enum class SortOrder { import androidx.annotation.StringRes
ALPHABETICAL, POPULARITY, UPDATED, NEWEST, RATING 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)
} }

@ -15,8 +15,9 @@ abstract class GroupleRepository(
protected abstract val domain: String protected abstract val domain: String
override val sortOrders = setOf( override val sortOrders = setOf(
SortOrder.ALPHABETICAL, SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.POPULARITY,
SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.RATING SortOrder.NEWEST, SortOrder.RATING
//FIXME SortOrder.ALPHABETICAL
) )
override suspend fun getList( override suspend fun getList(
@ -26,9 +27,16 @@ abstract class GroupleRepository(
tag: MangaTag? tag: MangaTag?
): List<Manga> { ): List<Manga> {
val doc = when { 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") 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() }.parseHtml()
val root = doc.body().getElementById("mangaBox") val root = doc.body().getElementById("mangaBox")
?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root") ?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root")
@ -128,14 +136,15 @@ abstract class GroupleRepository(
.selectFirst("table.table") .selectFirst("table.table")
return root.select("a.element-link").map { a -> return root.select("a.element-link").map { a ->
MangaTag( MangaTag(
title = a.text(), title = a.text().capitalize(),
key = a.attr("href").substringAfterLast('/'), key = a.attr("href").substringAfterLast('/'),
source = source source = source
) )
}.toSet() }.toSet()
} }
private fun getSortKey(sortOrder: SortOrder?) = when (sortOrder) { private fun getSortKey(sortOrder: SortOrder?) =
when (sortOrder ?: sortOrders.minBy { it.ordinal }) {
SortOrder.ALPHABETICAL -> "name" SortOrder.ALPHABETICAL -> "name"
SortOrder.POPULARITY -> "rate" SortOrder.POPULARITY -> "rate"
SortOrder.UPDATED -> "updated" SortOrder.UPDATED -> "updated"

@ -1,10 +1,12 @@
package org.koitharu.kotatsu.ui.common package org.koitharu.kotatsu.ui.common
import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import moxy.MvpAppCompatActivity import moxy.MvpAppCompatActivity
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent { abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
@ -27,4 +29,13 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
onBackPressed() onBackPressed()
true true
} else super.onOptionsItemSelected(item) } 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)
}
} }

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

@ -1,26 +1,19 @@
package org.koitharu.kotatsu.ui.common package org.koitharu.kotatsu.ui.common
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import moxy.MvpAppCompatFragment 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.ParcelableArgumentDelegate
import org.koitharu.kotatsu.utils.delegates.StringArgumentDelegate import org.koitharu.kotatsu.utils.delegates.StringArgumentDelegate
abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : abstract class BaseFragment(@LayoutRes contentLayoutId: Int) :
MvpAppCompatFragment(contentLayoutId), SharedPreferences.OnSharedPreferenceChangeListener { MvpAppCompatFragment(contentLayoutId) {
protected val settings by inject<AppSettings>()
fun stringArg(name: String) = StringArgumentDelegate(name) fun stringArg(name: String) = StringArgumentDelegate(name)
fun <T : Parcelable> arg(name: String) = ParcelableArgumentDelegate<T>(name) fun <T : Parcelable> arg(name: String) = ParcelableArgumentDelegate<T>(name)
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = Unit
open fun getTitle(): CharSequence? = null open fun getTitle(): CharSequence? = null
override fun onAttach(context: Context) { override fun onAttach(context: Context) {

@ -16,7 +16,7 @@ abstract class BaseViewHolder<T, E> protected constructor(view: View) :
override val containerView: View? override val containerView: View?
get() = itemView get() = itemView
protected var boundData: T? = null var boundData: T? = null
private set private set
val context get() = itemView.context!! val context get() = itemView.context!!

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

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

@ -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.graphics.Rect
import android.view.View import android.view.View

@ -33,7 +33,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
navigationView.setNavigationItemSelectedListener(this) navigationView.setNavigationItemSelectedListener(this)
if (!supportFragmentManager.isStateSaved) { if (supportFragmentManager.findFragmentById(R.id.container) == null) {
navigationView.setCheckedItem(R.id.nav_local_storage) navigationView.setCheckedItem(R.id.nav_local_storage)
setPrimaryFragment(LocalListFragment.newInstance()) setPrimaryFragment(LocalListFragment.newInstance())
} }

@ -6,28 +6,41 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.annotation.CallSuper
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga 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.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener 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.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.utils.ext.clearItemDecorations import org.koitharu.kotatsu.ui.main.list.filter.FilterAdapter
import org.koitharu.kotatsu.utils.ext.firstItem import org.koitharu.kotatsu.ui.main.list.filter.OnFilterChangedListener
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.hasItems
abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), MangaListView<E>, abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga> { PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener,
SectionItemDecoration.Callback {
private val settings by inject<AppSettings>()
private lateinit var adapter: MangaListAdapter 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
adapter = MangaListAdapter(this) adapter = MangaListAdapter(this)
initListMode(settings.listMode) initListMode(settings.listMode)
recyclerView.adapter = adapter recyclerView.adapter = adapter
@ -45,6 +59,8 @@ abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), Man
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
onRequestMoreItems(0) onRequestMoreItems(0)
} }
recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context))
recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this))
settings.subscribe(this) settings.subscribe(this)
} }
@ -55,7 +71,7 @@ abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), Man
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
if (!recyclerView.hasItems) { if (savedInstanceState?.containsKey("MoxyDelegateBundle") != true) {
onRequestMoreItems(0) onRequestMoreItems(0)
} }
} }
@ -70,9 +86,19 @@ abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), Man
ListModeSelectDialog.show(childFragmentManager) ListModeSelectDialog.show(childFragmentManager)
true true
} }
R.id.action_filter -> {
drawer.toggleDrawer(GravityCompat.END)
true
}
else -> super.onOptionsItemSelected(item) 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) { override fun onItemClick(item: Manga, position: Int, view: View) {
startActivity(MangaDetailsActivity.newIntent(context ?: return, item)) 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) { override fun onError(e: Exception) {
if (recyclerView.hasItems) { if (recyclerView.hasItems) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show() Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
.show()
} else { } else {
textView_holder.text = e.getDisplayMessage(resources) 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 layout_holder.isVisible = true
} }
} }
@ -117,6 +149,27 @@ abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), Man
} }
} }
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() { protected open fun setUpEmptyListHolder() {
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null) textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
textView_holder.setText(R.string.nothing_found) textView_holder.setText(R.string.nothing_found)
@ -134,12 +187,30 @@ abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), Man
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.addItemDecoration(when(mode) { recyclerView.addItemDecoration(
when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)
ListMode.DETAILED_LIST, ListMode.DETAILED_LIST,
ListMode.GRID -> SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)) ListMode.GRID -> SpacingItemDecoration(
}) resources.getDimensionPixelOffset(R.dimen.grid_spacing)
)
}
)
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
recyclerView.firstItem = position 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
}
}
} }

@ -10,14 +10,15 @@ import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.textAndVisible 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 private var coverRequest: RequestDisposable? = null
override fun onBind(data: Manga, extra: MangaHistory?) { override fun onBind(data: Manga, extra: MangaHistory?) {
coverRequest?.dispose() coverRequest?.dispose()
textView_title.text = data.title textView_title.text = data.title
textView_subtitle.textAndVisible = data.altTitle textView_subtitle.textAndVisible = data.tags.joinToString(", ") { it.title }
coverRequest = imageView_cover.load(data.coverUrl) { coverRequest = imageView_cover.load(data.coverUrl) {
crossfade(true) crossfade(true)
} }

@ -3,6 +3,9 @@ package org.koitharu.kotatsu.ui.main.list
import moxy.MvpView import moxy.MvpView
import moxy.viewstate.strategy.* import moxy.viewstate.strategy.*
import org.koitharu.kotatsu.core.model.Manga 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 { interface MangaListView<E> : MvpView {
@ -17,4 +20,7 @@ interface MangaListView<E> : MvpView {
@StateStrategyType(OneExecutionStateStrategy::class) @StateStrategyType(OneExecutionStateStrategy::class)
fun onError(e: Exception) fun onError(e: Exception)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onInitFilter(sortOrders: List<SortOrder>, tags: List<MangaTag>, currentFilter: MangaFilter?)
} }

@ -7,7 +7,6 @@ import androidx.core.util.set
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.disableFor
class CategoriesAdapter(private val listener: OnCategoryCheckListener) : class CategoriesAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() { BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
@ -37,7 +36,6 @@ class CategoriesAdapter(private val listener: OnCategoryCheckListener) :
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener if (it !is Checkable) return@setOnClickListener
it.toggle() it.toggle()
it.disableFor(200)
if (it.isChecked) { if (it.isChecked) {
listener.onCategoryChecked(holder.requireData()) listener.onCategoryChecked(holder.requireData())
} else { } else {

@ -1,13 +1,13 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.view.ViewGroup 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.R
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoryHolder(parent: ViewGroup) : 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) { override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title checkedTextView.text = data.title

@ -4,19 +4,18 @@ import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_favorite_categories.* import kotlinx.android.synthetic.main.dialog_favorite_categories.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga 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.ui.common.TextInputDialog
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesDialog() : AlertDialogFragment(R.layout.dialog_favorite_categories), class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_categories),
FavouriteCategoriesView, FavouriteCategoriesView,
OnCategoryCheckListener { OnCategoryCheckListener {
@ -26,10 +25,6 @@ class FavouriteCategoriesDialog() : AlertDialogFragment(R.layout.dialog_favorite
private var adapter: CategoriesAdapter? = null 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = CategoriesAdapter(this) adapter = CategoriesAdapter(this)

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

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

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

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

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.ui.main.list.remote package org.koitharu.kotatsu.ui.main.list.remote
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@ -19,6 +20,11 @@ class RemoteListFragment : MangaListFragment<Unit>() {
return source.title return source.title
} }
override fun onFilterChanged(filter: MangaFilter) {
presenter.applyFilter(source, filter)
super.onFilterChanged(filter)
}
companion object { companion object {
private const val ARG_SOURCE = "provider" private const val ARG_SOURCE = "provider"

@ -5,6 +5,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import moxy.InjectViewState import moxy.InjectViewState
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
@ -13,13 +14,19 @@ import org.koitharu.kotatsu.ui.main.list.MangaListView
@InjectViewState @InjectViewState
class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() { class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() {
private var isFilterInitialized = false
private var filter: MangaFilter? = null
fun loadList(source: MangaSource, offset: Int) { fun loadList(source: MangaSource, offset: Int) {
launch { launch {
viewState.onLoadingChanged(true) viewState.onLoadingChanged(true)
try { try {
val list = withContext(Dispatchers.IO) { val list = withContext(Dispatchers.IO) {
MangaProviderFactory.create(source) MangaProviderFactory.create(source).getList(
.getList(offset) offset = offset,
sortOrder = filter?.sortOrder,
tag = filter?.tag
)
} }
if (offset == 0) { if (offset == 0) {
viewState.onListChanged(list) viewState.onListChanged(list)
@ -35,5 +42,32 @@ class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() {
viewState.onLoadingChanged(false) 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
}
}
} }
} }

@ -13,6 +13,7 @@ import androidx.appcompat.widget.PopupMenu
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -132,3 +133,11 @@ fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
getGlobalVisibleRect(rect) getGlobalVisibleRect(rect)
return rect.contains(x, y) return rect.contains(x, y)
} }
fun DrawerLayout.toggleDrawer(gravity: Int) {
if (isDrawerOpen(gravity)) {
closeDrawer(gravity)
} else {
openDrawer(gravity)
}
}

@ -5,8 +5,15 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:paddingTop="12dp">
<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 <View
android:layout_width="match_parent" android:layout_width="match_parent"
@ -20,7 +27,7 @@
android:orientation="vertical" android:orientation="vertical"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_caegory_checkable" /> tools:listitem="@layout/item_category_checkable" />
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

@ -1,8 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:openDrawer="end">
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -17,7 +23,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_manga_list" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@ -26,9 +33,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center_horizontal"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:gravity="center_horizontal"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@ -38,7 +45,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" android:gravity="center"
android:text="?android:textColorSecondary" android:text="?android:textColorSecondary"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textAppearance="?android:textAppearanceMedium"
tools:text="@tools:sample/lorem[3]" /> tools:text="@tools:sample/lorem[3]" />
</LinearLayout> </LinearLayout>
@ -51,3 +58,16 @@
android:indeterminate="true" /> android:indeterminate="true" />
</FrameLayout> </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>

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

@ -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]" />

@ -1,9 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android" 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" 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 <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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_width="match_parent"
android:layout_height="@dimen/manga_list_item_height" android:layout_height="@dimen/manga_list_item_height"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
@ -8,8 +9,8 @@
<org.koitharu.kotatsu.ui.common.widgets.CoverImageView <org.koitharu.kotatsu.ui.common.widgets.CoverImageView
android:id="@+id/imageView_cover" android:id="@+id/imageView_cover"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:orientation="vertical" android:layout_height="match_parent"
android:layout_height="match_parent" /> android:orientation="vertical" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

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

@ -5,7 +5,13 @@
<item <item
android:id="@+id/action_list_mode" android:id="@+id/action_list_mode"
android:title="@string/list_mode"
android:orderInCategory="20" 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" /> app:showAsAction="never" />
</menu> </menu>

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

@ -46,4 +46,13 @@
<string name="save_this_chapter_and_next">Save this chapter and next</string> <string name="save_this_chapter_and_next">Save this chapter and next</string>
<string name="save_this_chapter">Save this chapter</string> <string name="save_this_chapter">Save this chapter</string>
<string name="no_saved_manga">No saved manga</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> </resources>
Loading…
Cancel
Save