().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
+ return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
}
override fun searchMangaSelector() = popularMangaSelector()
- override fun searchMangaFromElement(element: Element): SManga {
- return popularMangaFromElement(element)
- }
+ override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
// max 200 results
override fun searchMangaNextPageSelector() = null
@@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() {
return manga
}
- private fun parseStatus(element: String): Int {
- when {
- element.contains("Запрещена публикация произведения по копирайту
") -> return SManga.LICENSED
- element.contains(" Сингл") || element.contains("Перевод: завершен") -> return SManga.COMPLETED
- element.contains("Перевод: продолжается") -> return SManga.ONGOING
- else -> return SManga.UNKNOWN
- }
+ private fun parseStatus(element: String): Int = when {
+ element.contains("Запрещена публикация произведения по копирайту
") -> SManga.LICENSED
+ element.contains(" Сингл") || element.contains("Перевод: завершен") -> SManga.COMPLETED
+ element.contains("Перевод: продолжается") -> SManga.ONGOING
+ else -> SManga.UNKNOWN
}
override fun chapterListSelector() = "div.chapters-link tbody tr"
@@ -149,7 +142,7 @@ class Readmanga : ParsedHttpSource() {
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
- * on http://readmanga.me/search
+ * on http://readmanga.me/search/advanced
*/
override fun getFilterList() = FilterList(
Genre("арт", "el_5685"),
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
index 9f55cd033..8ab55d9f7 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.os.Bundle
-import android.support.v4.view.MenuItemCompat
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.MenuItem
@@ -34,7 +33,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
return null
}
- private fun setTitle() {
+ fun setTitle() {
var parentController = parentController
while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) {
@@ -52,7 +51,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
* Issue link: https://issuetracker.google.com/issues/37657375
*/
fun MenuItem.fixExpand() {
- val expandListener = object : MenuItemCompat.OnActionExpandListener {
+ setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
@@ -61,8 +60,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
activity?.invalidateOptionsMenu()
return true
}
- }
- MenuItemCompat.setOnActionExpandListener(this, expandListener)
+ })
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
index 63eba25ed..3f252409c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
@@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter
@Suppress("LeakingThis")
-abstract class NucleusController
>(val bundle: Bundle? = null) : RxController(),
+abstract class NucleusController
>(val bundle: Bundle? = null) : RxController(bundle),
PresenterFactory
{
private val delegate = NucleusConductorDelegate(this)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java
index 62a50af83..ddc4aba5a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java
@@ -10,7 +10,6 @@ public class NucleusConductorDelegate
{
@Nullable private P presenter;
@Nullable private Bundle bundle;
- private boolean presenterHasView = false;
private PresenterFactory
factory;
@@ -22,8 +21,8 @@ public class NucleusConductorDelegate
{
if (presenter == null) {
presenter = factory.createPresenter();
presenter.create(bundle);
+ bundle = null;
}
- bundle = null;
return presenter;
}
@@ -37,31 +36,26 @@ public class NucleusConductorDelegate
{
}
void onRestoreInstanceState(Bundle presenterState) {
- if (presenter != null)
- throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()");
bundle = presenterState;
}
void onTakeView(Object view) {
getPresenter();
- if (presenter != null && !presenterHasView) {
+ if (presenter != null) {
//noinspection unchecked
presenter.takeView(view);
- presenterHasView = true;
}
}
void onDropView() {
- if (presenter != null && presenterHasView) {
+ if (presenter != null) {
presenter.dropView();
- presenterHasView = false;
}
}
void onDestroy() {
if (presenter != null) {
presenter.destroy();
- presenter = null;
}
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
index cac5a12a9..2df28474e 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
@@ -4,24 +4,20 @@ import android.content.res.Configuration
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
-import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.*
import android.view.*
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
-import com.jakewharton.rxbinding.widget.itemSelections
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
@@ -43,14 +39,18 @@ import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
*/
-open class CatalogueController(bundle: Bundle? = null) :
+open class CatalogueController(bundle: Bundle) :
NucleusController(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
- FlexibleAdapter.EndlessScrollListener,
+ FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener {
+ constructor(source: CatalogueSource) : this(Bundle().apply {
+ putLong(SOURCE_ID_KEY, source.id)
+ })
+
/**
* Preferences helper.
*/
@@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private var adapter: FlexibleAdapter>? = null
- /**
- * Spinner shown in the toolbar to change the selected source.
- */
- private var spinner: Spinner? = null
-
/**
* Snackbar containing an error message when a request fails.
*/
@@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private var recycler: RecyclerView? = null
+ /**
+ * Drawer listener to allow swipe only for closing the drawer.
+ */
private var drawerListener: DrawerLayout.DrawerListener? = null
- /**
- * Query of the search box.
- */
- private val query: String
- get() = presenter.query
-
- /**
- * Selected index of the spinner (selected source).
- */
- private var selectedIndex: Int = 0
-
/**
* Subscription for the search view.
*/
private var searchViewSubscription: Subscription? = null
+ /**
+ * Subscription for the number of manga per row.
+ */
private var numColumnsSubscription: Subscription? = null
+ /**
+ * Endless loading item.
+ */
private var progressItem: ProgressItem? = null
init {
@@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
}
override fun getTitle(): String? {
- return ""
+ return presenter.source.name
}
override fun createPresenter(): CataloguePresenter {
- return CataloguePresenter()
+ return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
- // Create toolbar spinner
- val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
- ?: activity
-
- val spinnerAdapter = ArrayAdapter(themedContext,
- android.R.layout.simple_spinner_item, presenter.sources)
- spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item)
-
- val onItemSelected: (Int) -> Unit = { position ->
- val source = spinnerAdapter.getItem(position)
- if (!presenter.isValidSource(source)) {
- spinner?.setSelection(selectedIndex)
- activity?.toast(R.string.source_requires_login)
- } else if (source != presenter.source) {
- selectedIndex = position
- showProgressBar()
- adapter?.clear()
- presenter.setActiveSource(source)
- navView?.setFilters(presenter.filterItems)
- activity?.invalidateOptionsMenu()
- }
- }
-
- selectedIndex = presenter.sources.indexOf(presenter.source)
-
- spinner = Spinner(themedContext).apply {
- adapter = spinnerAdapter
- setSelection(selectedIndex)
- itemSelections()
- .skip(1)
- .filter { it != AdapterView.INVALID_POSITION }
- .subscribeUntilDestroy { onItemSelected(it) }
- }
-
- activity?.toolbar?.addView(spinner)
+ navView?.setFilters(presenter.filterItems)
view.progress?.visible()
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
- activity?.toolbar?.removeView(spinner)
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
- spinner = null
snack = null
recycler = null
}
@@ -187,10 +144,7 @@ open class CatalogueController(bundle: Bundle? = null) :
}
navView.setFilters(presenter.filterItems)
- navView.post {
- if (isAttached && !drawer.isDrawerOpen(navView))
- drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
- }
+ drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
@@ -228,6 +182,7 @@ open class CatalogueController(bundle: Bundle? = null) :
val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply {
+ id = R.id.recycler
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
@@ -267,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
+ val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
@@ -330,9 +286,14 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
- if (query == newQuery)
+ if (presenter.query == newQuery)
return
+ // FIXME dirty fix to restore the toolbar buttons after closing search mode.
+ if (newQuery == "") {
+ activity?.invalidateOptionsMenu()
+ }
+
showProgressBar()
adapter?.clear()
@@ -444,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
- presenter.prefs.portraitColumns()
+ preferences.portraitColumns()
else
- presenter.prefs.landscapeColumns()
+ preferences.landscapeColumns()
}
/**
@@ -555,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
presenter.updateMangaCategories(manga, categories)
}
+ protected companion object {
+ const val SOURCE_ID_KEY = "sourceId"
+ }
+
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt
index 4cd6554fa..3fdba1e2e 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt
@@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
-import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
@@ -36,16 +36,15 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
}
override fun setImage(manga: Manga) {
- Glide.clear(view.thumbnail)
+ GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) {
- Glide.with(view.context)
+ GlideApp.with(view.context)
.load(manga)
- .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
.into(StateImageViewTarget(view.thumbnail, view.progress))
-
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt
index b6207b8a1..0a3209810 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt
@@ -1,39 +1,40 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.Gravity
-import android.view.LayoutInflater
-import android.view.ViewGroup
+import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
+import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
-class CatalogueItem(val manga: Manga) : AbstractFlexibleItem() {
+class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference) :
+ AbstractFlexibleItem() {
override fun getLayoutRes(): Int {
- return R.layout.catalogue_grid_item
+ return if (catalogueAsList.getOrDefault())
+ R.layout.catalogue_list_item
+ else
+ R.layout.catalogue_grid_item
}
- override fun createViewHolder(adapter: FlexibleAdapter<*>,
- inflater: LayoutInflater,
- parent: ViewGroup): CatalogueHolder {
-
- if (parent is AutofitRecyclerView) {
- val view = parent.inflate(R.layout.catalogue_grid_item).apply {
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueHolder {
+ val parent = adapter.recyclerView
+ return if (parent is AutofitRecyclerView) {
+ view.apply {
card.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4)
gradient.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
}
- return CatalogueGridHolder(view, adapter)
+ CatalogueGridHolder(view, adapter)
} else {
- val view = parent.inflate(R.layout.catalogue_list_item)
- return CatalogueListHolder(view, adapter)
+ CatalogueListHolder(view, adapter)
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt
index 5b782b167..a12ec77d2 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt
@@ -1,12 +1,11 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
-import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.getResourceColor
-import jp.wasabeef.glide.transformations.CropCircleTransformation
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
/**
@@ -37,13 +36,13 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
}
override fun setImage(manga: Manga) {
- Glide.clear(view.thumbnail)
+ GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) {
- Glide.with(view.context)
+ GlideApp.with(view.context)
.load(manga)
- .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop()
- .bitmapTransform(CropCircleTransformation(view.context))
+ .circleCrop()
.dontAnimate()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt
index 1bf4d3c0f..c55727129 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt
@@ -34,7 +34,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
}
fun setFilters(items: List>) {
- adapter.updateDataSet(items.toMutableList())
+ adapter.updateDataSet(items)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
index 69fd34db7..62a6977ba 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
@@ -9,15 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
-import eu.kanade.tachiyomi.source.LocalSource
-import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable
@@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
* Presenter of [CatalogueController].
*/
open class CataloguePresenter(
- val sourceManager: SourceManager = Injekt.get(),
- val db: DatabaseHelper = Injekt.get(),
- val prefs: PreferencesHelper = Injekt.get(),
- val coverCache: CoverCache = Injekt.get()
+ sourceId: Long,
+ sourceManager: SourceManager = Injekt.get(),
+ private val db: DatabaseHelper = Injekt.get(),
+ private val prefs: PreferencesHelper = Injekt.get(),
+ private val coverCache: CoverCache = Injekt.get()
) : BasePresenter() {
/**
- * Enabled sources.
+ * Selected source.
*/
- val sources by lazy { getEnabledSources() }
-
- /**
- * Active source.
- */
- lateinit var source: CatalogueSource
- private set
+ val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Query from the view.
@@ -106,7 +97,6 @@ open class CataloguePresenter(
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
- source = getLastUsedSource()
sourceFilters = source.getFilterList()
if (savedState != null) {
@@ -141,17 +131,19 @@ open class CataloguePresenter(
val sourceId = source.id
+ val catalogueAsList = prefs.catalogueAsList()
+
// Prepare the pager.
pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results()
.observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) }
- .map { it.first to it.second.map(::CatalogueItem) }
+ .map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread())
- .subscribeReplay({ view, pair ->
- view.onAddPage(pair.first, pair.second)
- }, { view, error ->
+ .subscribeReplay({ view, (page, mangas) ->
+ view.onAddPage(page, mangas)
+ }, { _, error ->
Timber.e(error)
})
@@ -167,7 +159,7 @@ open class CataloguePresenter(
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
- .subscribeFirst({ view, page ->
+ .subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError)
}
@@ -179,19 +171,6 @@ open class CataloguePresenter(
return pager.hasNextPage
}
- /**
- * Sets the active source and restarts the pager.
- *
- * @param source the new active source.
- */
- fun setActiveSource(source: CatalogueSource) {
- prefs.lastUsedCatalogueSource().set(source.id)
- this.source = source
- sourceFilters = source.getFilterList()
-
- restartPager(query = "", filters = FilterList())
- }
-
/**
* Sets the display mode.
*
@@ -267,50 +246,6 @@ open class CataloguePresenter(
.onErrorResumeNext { Observable.just(manga) }
}
- /**
- * Returns the last used source from preferences or the first valid source.
- *
- * @return a source.
- */
- fun getLastUsedSource(): CatalogueSource {
- val id = prefs.lastUsedCatalogueSource().get() ?: -1
- val source = sourceManager.get(id)
- if (!isValidSource(source) || source !in sources) {
- return sources.first { isValidSource(it) }
- }
- return source as CatalogueSource
- }
-
- /**
- * Checks if the given source is valid.
- *
- * @param source the source to check.
- * @return true if the source is valid, false otherwise.
- */
- open fun isValidSource(source: Source?): Boolean {
- if (source == null) return false
-
- if (source is LoginSource) {
- return source.isLogged() ||
- (prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "")
- }
- return true
- }
-
- /**
- * Returns a list of enabled sources ordered by language and name.
- */
- open protected fun getEnabledSources(): List {
- val languages = prefs.enabledLanguages().getOrDefault()
- val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
-
- return sourceManager.getCatalogueSources()
- .filter { it.lang in languages }
- .filterNot { it.id.toString() in hiddenCatalogues }
- .sortedBy { "(${it.lang}) ${it.name}" } +
- sourceManager.get(LocalSource.ID) as LocalSource
- }
-
/**
* Adds or removes a manga from the library.
*
@@ -370,13 +305,12 @@ open class CataloguePresenter(
}
is Filter.Sort -> {
val group = SortGroup(it)
- val subItems = it.values.mapNotNull {
+ val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
- else -> null
}
}
}
@@ -407,7 +341,7 @@ open class CataloguePresenter(
* @param categories the selected categories.
* @param manga the manga to move.
*/
- fun moveMangaToCategories(manga: Manga, categories: List) {
+ private fun moveMangaToCategories(manga: Manga, categories: List) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt
index 1f1d75b72..279017a74 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt
@@ -1,30 +1,27 @@
package eu.kanade.tachiyomi.ui.catalogue
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
class ProgressItem : AbstractFlexibleItem() {
- var loadMore = true
+ private var loadMore = true
override fun getLayoutRes(): Int {
return R.layout.catalogue_progress_item
}
- override fun createViewHolder(adapter: FlexibleAdapter>, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
- override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List) {
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List) {
holder.progressBar.visibility = View.GONE
holder.progressMessage.visibility = View.GONE
@@ -45,8 +42,8 @@ class ProgressItem : AbstractFlexibleItem() {
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
- val progressBar = view.findViewById(R.id.progress_bar) as ProgressBar
- val progressMessage = view.findViewById(R.id.progress_message) as TextView
+ val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
+ val progressMessage: TextView = view.findViewById(R.id.progress_message)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt
index d9bab855e..5ad10faf8 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt
@@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import android.widget.CheckBox
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@@ -16,8 +14,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -32,10 +30,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) {
- val check = itemView.findViewById(R.id.nav_view_item) as CheckBox
+ val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt
index c023ce596..f5839fa98 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt
@@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -19,8 +17,12 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun getItemViewType(): Int {
+ return 101
+ }
+
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -34,10 +36,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem) : AbstractExpandableHeaderItem) : ExpandableViewHolder(view, adapter, true) {
- val title = itemView.findViewById(R.id.title) as TextView
- val icon = itemView.findViewById(R.id.expand_icon) as ImageView
+ val title: TextView = itemView.findViewById(R.id.title)
+ val icon: ImageView = itemView.findViewById(R.id.expand_icon)
override fun shouldNotifyParentOnClick(): Boolean {
return true
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt
index a76612167..fc929af2e 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt
@@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint
import android.support.design.R
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
@@ -18,8 +16,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -29,10 +27,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem) : SelectItem(filter), ISection
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other is SelectSectionItem) {
- return filter == other.filter
- }
- return false
+ if (javaClass != other?.javaClass) return false
+ return filter == (other as SelectSectionItem).filter
}
override fun hashCode(): Int {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt
index ad840475e..6a3e9005e 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt
@@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
@@ -19,8 +17,8 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -32,18 +30,16 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem
- filter.state = position
+ spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos ->
+ filter.state = pos
}
spinner.setSelection(filter.state)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other is SelectItem) {
- return filter == other.filter
- }
- return false
+ if (javaClass != other?.javaClass) return false
+ return filter == (other as SelectItem).filter
}
override fun hashCode(): Int {
@@ -52,7 +48,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) {
- val text = itemView.findViewById(R.id.nav_view_item_text) as TextView
- val spinner = itemView.findViewById(R.id.nav_view_item) as Spinner
+ val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
+ val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt
index 8420f2f7d..61fa80c8b 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt
@@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint
import android.support.design.R
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.viewholders.FlexibleViewHolder
@@ -17,8 +15,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -27,10 +25,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem>() {
- // Use an id instead of the layout res to allow to reuse the layout.
override fun getLayoutRes(): Int {
- return R.id.catalogue_filter_sort_group
+ return R.layout.navigation_view_group
}
- override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(R.layout.navigation_view_group, parent, false), adapter)
+ override fun getItemViewType(): Int {
+ return 100
+ }
+
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -32,10 +33,8 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem(group) {
- // Use an id instead of the layout res to allow to reuse the layout.
override fun getLayoutRes(): Int {
- return R.id.catalogue_filter_sort_item
+ return R.layout.navigation_view_checkedtext
}
- override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(R.layout.navigation_view_checkedtext, parent, false), adapter)
+ override fun getItemViewType(): Int {
+ return 102
+ }
+
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -54,10 +55,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other is SortItem) {
- return name == other.name && group == other.group
- }
- return false
+ if (javaClass != other?.javaClass) return false
+ other as SortItem
+ return name == other.name && group == other.group
}
override fun hashCode(): Int {
@@ -68,7 +68,7 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
- val text = itemView.findViewById(R.id.nav_view_item) as CheckedTextView
+ val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt
index 9d4321dcb..18c57b640 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt
@@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.widget.TextInputLayout
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import android.widget.EditText
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@@ -18,8 +16,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -34,10 +32,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) {
- val wrapper = itemView.findViewById(R.id.nav_view_item_wrapper) as TextInputLayout
- val edit = itemView.findViewById(R.id.nav_view_item) as EditText
+ val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper)
+ val edit: EditText = itemView.findViewById(R.id.nav_view_item)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt
index 0c834b337..d122251c9 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt
@@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.R
import android.support.graphics.drawable.VectorDrawableCompat
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
import android.widget.CheckedTextView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@@ -20,8 +18,12 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup?): Holder {
- return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+ override fun getItemViewType(): Int {
+ return 103
+ }
+
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
+ return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) {
@@ -51,10 +53,8 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) {
- val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView
+ val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
init {
// Align with native checkbox
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt
new file mode 100644
index 000000000..0b1b822e0
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt
@@ -0,0 +1,74 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.support.v7.widget.RecyclerView
+import android.util.SparseArray
+import eu.davidea.flexibleadapter.FlexibleAdapter
+
+/**
+ * Adapter that holds the search cards.
+ *
+ * @param controller instance of [CatalogueSearchController].
+ */
+class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
+ FlexibleAdapter(null, controller, true) {
+
+ /**
+ * Bundle where the view state of the holders is saved.
+ */
+ private var bundle = Bundle()
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List?) {
+ super.onBindViewHolder(holder, position, payloads)
+ restoreHolderState(holder)
+ }
+
+ override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
+ super.onViewRecycled(holder)
+ saveHolderState(holder, bundle)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ val holdersBundle = Bundle()
+ allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) }
+ outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)
+ }
+
+ /**
+ * Saves the view state of the given holder.
+ *
+ * @param holder The holder to save.
+ * @param outState The bundle where the state is saved.
+ */
+ private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
+ val key = "holder_${holder.adapterPosition}"
+ val holderState = SparseArray()
+ holder.itemView.saveHierarchyState(holderState)
+ outState.putSparseParcelableArray(key, holderState)
+ }
+
+ /**
+ * Restores the view state of the given holder.
+ *
+ * @param holder The holder to restore.
+ */
+ private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
+ val key = "holder_${holder.adapterPosition}"
+ val holderState = bundle.getSparseParcelableArray(key)
+ if (holderState != null) {
+ holder.itemView.restoreHierarchyState(holderState)
+ bundle.remove(key)
+ }
+ }
+
+ private companion object {
+ const val HOLDER_BUNDLE_KEY = "holder_bundle"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt
new file mode 100644
index 000000000..17791f3be
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt
@@ -0,0 +1,27 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+/**
+ * Adapter that holds the manga items from search results.
+ *
+ * @param controller instance of [CatalogueSearchController].
+ */
+class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
+ FlexibleAdapter(null, controller, true) {
+
+ /**
+ * Listen for browse item clicks.
+ */
+ val mangaClickListener: OnMangaClickListener = controller
+
+ /**
+ * Listener which should be called when user clicks browse.
+ * Note: Should only be handled by [CatalogueSearchController]
+ */
+ interface OnMangaClickListener {
+ fun onMangaClick(manga: Manga)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt
new file mode 100644
index 000000000..e35a2ceb7
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt
@@ -0,0 +1,43 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.View
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.widget.StateImageViewTarget
+import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.*
+
+class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
+ : FlexibleViewHolder(view, adapter) {
+
+ init {
+ // Call onMangaClickListener when item is pressed.
+ itemView.setOnClickListener {
+ val item = adapter.getItem(adapterPosition)
+ if (item != null) {
+ adapter.mangaClickListener.onMangaClick(item.manga)
+ }
+ }
+ }
+
+ fun bind(manga: Manga) {
+ itemView.tvTitle.text = manga.title
+
+ setImage(manga)
+ }
+
+ fun setImage(manga: Manga) {
+ GlideApp.with(itemView.context).clear(itemView.itemImage)
+ if (!manga.thumbnail_url.isNullOrEmpty()) {
+ GlideApp.with(itemView.context)
+ .load(manga)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ .centerCrop()
+ .skipMemoryCache(true)
+ .placeholder(android.R.color.transparent)
+ .into(StateImageViewTarget(itemView.itemImage, itemView.progress))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt
new file mode 100644
index 000000000..3d43b12e9
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt
@@ -0,0 +1,35 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem() {
+
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_global_search_controller_card_item
+ }
+
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchCardHolder {
+ return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter)
+ }
+
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder,
+ position: Int, payloads: List?) {
+ holder.bind(manga)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is CatalogueSearchCardItem) {
+ return manga.id == other.manga.id
+ }
+ return false
+ }
+
+ override fun hashCode(): Int {
+ return manga.id?.toInt() ?: 0
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt
new file mode 100644
index 000000000..dcbb42f4c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt
@@ -0,0 +1,184 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.SearchView
+import android.view.*
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.*
+
+/**
+ * This controller shows and manages the different search result in global search.
+ * This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
+ * [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
+ */
+class CatalogueSearchController(private val initialQuery: String? = null) :
+ NucleusController(),
+ CatalogueSearchCardAdapter.OnMangaClickListener {
+
+ /**
+ * Adapter containing search results grouped by lang.
+ */
+ private var adapter: CatalogueSearchAdapter? = null
+
+ /**
+ * Called when controller is initialized.
+ */
+ init {
+ setHasOptionsMenu(true)
+ }
+
+ /**
+ * Initiate the view with [R.layout.catalogue_global_search_controller].
+ *
+ * @param inflater used to load the layout xml.
+ * @param container containing parent views.
+ * @return inflated view
+ */
+ override fun inflateView(inflater: LayoutInflater, container: ViewGroup): android.view.View {
+ return inflater.inflate(R.layout.catalogue_global_search_controller, container, false)
+ }
+
+ /**
+ * Set the title of controller.
+ *
+ * @return title.
+ */
+ override fun getTitle(): String? {
+ return presenter.query
+ }
+
+ /**
+ * Create the [CatalogueSearchPresenter] used in controller.
+ *
+ * @return instance of [CatalogueSearchPresenter]
+ */
+ override fun createPresenter(): CatalogueSearchPresenter {
+ return CatalogueSearchPresenter(initialQuery)
+ }
+
+ /**
+ * Called when manga in global search is clicked, opens manga.
+ *
+ * @param manga clicked item containing manga information.
+ */
+ override fun onMangaClick(manga: Manga) {
+ // Open MangaController.
+ router.pushController(RouterTransaction.with(MangaController(manga, true))
+ .pushChangeHandler(FadeChangeHandler())
+ .popChangeHandler(FadeChangeHandler()))
+ }
+
+ /**
+ * Adds items to the options menu.
+ *
+ * @param menu menu containing options.
+ * @param inflater used to load the menu xml.
+ */
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ // Inflate menu.
+ inflater.inflate(R.menu.catalogue_new_list, menu)
+
+ // Initialize search menu
+ val searchItem = menu.findItem(R.id.action_search)
+ val searchView = searchItem.actionView as SearchView
+
+ searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
+ override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ searchView.onActionViewExpanded() // Required to show the query in the view
+ searchView.setQuery(presenter.query, false)
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
+ return true
+ }
+ })
+
+ searchView.queryTextChangeEvents()
+ .filter { it.isSubmitted }
+ .subscribeUntilDestroy {
+ presenter.search(it.queryText().toString())
+ searchItem.collapseActionView()
+ setTitle() // Update toolbar title
+ }
+ }
+
+ /**
+ * Called when the view is created
+ *
+ * @param view view of controller
+ * @param savedViewState information from previous state.
+ */
+ override fun onViewCreated(view: View, savedViewState: Bundle?) {
+ super.onViewCreated(view, savedViewState)
+
+ adapter = CatalogueSearchAdapter(this)
+
+ with(view) {
+ // Create recycler and set adapter.
+ recycler.layoutManager = LinearLayoutManager(context)
+ recycler.adapter = adapter
+ }
+ }
+
+ override fun onDestroyView(view: View) {
+ adapter = null
+ super.onDestroyView(view)
+ }
+
+ override fun onSaveViewState(view: View, outState: Bundle) {
+ super.onSaveViewState(view, outState)
+ adapter?.onSaveInstanceState(outState)
+ }
+
+ override fun onRestoreViewState(view: View, savedViewState: Bundle) {
+ super.onRestoreViewState(view, savedViewState)
+ adapter?.onRestoreInstanceState(savedViewState)
+ }
+
+ /**
+ * Returns the view holder for the given manga.
+ *
+ * @param source used to find holder containing source
+ * @return the holder of the manga or null if it's not bound.
+ */
+ private fun getHolder(source: CatalogueSource): CatalogueSearchHolder? {
+ val adapter = adapter ?: return null
+
+ adapter.allBoundViewHolders.forEach { holder ->
+ val item = adapter.getItem(holder.adapterPosition)
+ if (item != null && source.id == item.source.id) {
+ return holder as CatalogueSearchHolder
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Add search result to adapter.
+ *
+ * @param searchResult result of search.
+ */
+ fun setItems(searchResult: List) {
+ adapter?.updateDataSet(searchResult)
+ }
+
+ /**
+ * Called from the presenter when a manga is initialized.
+ *
+ * @param manga the initialized manga.
+ */
+ fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
+ getHolder(source)?.setImage(manga)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt
new file mode 100644
index 000000000..0714c4342
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt
@@ -0,0 +1,100 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.support.v7.widget.LinearLayoutManager
+import android.view.View
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.setVectorCompat
+import eu.kanade.tachiyomi.util.visible
+import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
+
+/**
+ * Holder that binds the [CatalogueSearchItem] containing catalogue cards.
+ *
+ * @param view view of [CatalogueSearchItem]
+ * @param adapter instance of [CatalogueSearchAdapter]
+ */
+class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) {
+
+ /**
+ * Adapter containing manga from search results.
+ */
+ private val mangaAdapter = CatalogueSearchCardAdapter(adapter.controller)
+
+ private var lastBoundResults: List? = null
+
+ init {
+ with(itemView) {
+ // Set layout horizontal.
+ recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ recycler.adapter = mangaAdapter
+
+ nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
+ context.getResourceColor(android.R.attr.textColorHint))
+ }
+ }
+
+ /**
+ * Show the loading of source search result.
+ *
+ * @param item item of card.
+ */
+ fun bind(item: CatalogueSearchItem) {
+ val source = item.source
+ val results = item.results
+
+ with(itemView) {
+ // Set Title witch country code if available.
+ title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
+
+ when {
+ results == null -> {
+ progress.visible()
+ nothing_found.gone()
+ }
+ results.isEmpty() -> {
+ progress.gone()
+ nothing_found.visible()
+ }
+ else -> {
+ progress.gone()
+ nothing_found.gone()
+ }
+ }
+ if (results !== lastBoundResults) {
+ mangaAdapter.updateDataSet(results)
+ lastBoundResults = results
+ }
+ }
+ }
+
+ /**
+ * Called from the presenter when a manga is initialized.
+ *
+ * @param manga the initialized manga.
+ */
+ fun setImage(manga: Manga) {
+ getHolder(manga)?.setImage(manga)
+ }
+
+ /**
+ * Returns the view holder for the given manga.
+ *
+ * @param manga the manga to find.
+ * @return the holder of the manga or null if it's not bound.
+ */
+ private fun getHolder(manga: Manga): CatalogueSearchCardHolder? {
+ mangaAdapter.allBoundViewHolders.forEach { holder ->
+ val item = mangaAdapter.getItem(holder.adapterPosition)
+ if (item != null && item.manga.id!! == manga.id!!) {
+ return holder as CatalogueSearchCardHolder
+ }
+ }
+
+ return null
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt
new file mode 100644
index 000000000..7722a3202
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt
@@ -0,0 +1,64 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.CatalogueSource
+
+/**
+ * Item that contains search result information.
+ *
+ * @param source contains information about search result.
+ */
+class CatalogueSearchItem(val source: CatalogueSource, val results: List?)
+ : AbstractFlexibleItem() {
+
+ /**
+ * Set view.
+ *
+ * @return id of view
+ */
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_global_search_controller_card
+ }
+
+ /**
+ * Create view holder (see [CatalogueSearchAdapter].
+ *
+ * @return holder of view.
+ */
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchHolder {
+ return CatalogueSearchHolder(view, adapter as CatalogueSearchAdapter)
+ }
+
+ /**
+ * Bind item to view.
+ */
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder,
+ position: Int, payloads: List?) {
+ holder.bind(this)
+ }
+
+ /**
+ * Used to check if two items are equal.
+ *
+ * @return items are equal?
+ */
+ override fun equals(other: Any?): Boolean {
+ if (other is CatalogueSearchItem) {
+ return source.id == other.source.id
+ }
+ return false
+ }
+
+ /**
+ * Return hash code of item.
+ *
+ * @return hashcode
+ */
+ override fun hashCode(): Int {
+ return source.id.toInt()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt
new file mode 100644
index 000000000..ebd5327d6
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt
@@ -0,0 +1,215 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import rx.subjects.PublishSubject
+import timber.log.Timber
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+/**
+ * Presenter of [CatalogueSearchController]
+ * Function calls should be done from here. UI calls should be done from the controller.
+ *
+ * @param sourceManager manages the different sources.
+ * @param db manages the database calls.
+ * @param preferencesHelper manages the preference calls.
+ */
+class CatalogueSearchPresenter(
+ val initialQuery: String? = "",
+ val sourceManager: SourceManager = Injekt.get(),
+ val db: DatabaseHelper = Injekt.get(),
+ val preferencesHelper: PreferencesHelper = Injekt.get()
+) : BasePresenter() {
+
+ /**
+ * Enabled sources.
+ */
+ val sources by lazy { getEnabledSources() }
+
+ /**
+ * Query from the view.
+ */
+ var query = ""
+ private set
+
+ /**
+ * Fetches the different sources by user settings.
+ */
+ private var fetchSourcesSubscription: Subscription? = null
+
+ /**
+ * Subject which fetches image of given manga.
+ */
+ private val fetchImageSubject = PublishSubject.create, Source>>()
+
+ /**
+ * Subscription for fetching images of manga.
+ */
+ private var fetchImageSubscription: Subscription? = null
+
+ override fun onCreate(savedState: Bundle?) {
+ super.onCreate(savedState)
+
+ // Perform a search with previous or initial state
+ search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty())
+ }
+
+ override fun onDestroy() {
+ fetchSourcesSubscription?.unsubscribe()
+ fetchImageSubscription?.unsubscribe()
+ super.onDestroy()
+ }
+
+ override fun onSave(state: Bundle) {
+ state.putString(CataloguePresenter::query.name, query)
+ super.onSave(state)
+ }
+
+ /**
+ * Returns a list of enabled sources ordered by language and name.
+ *
+ * @return list containing enabled sources.
+ */
+ private fun getEnabledSources(): List {
+ val languages = preferencesHelper.enabledLanguages().getOrDefault()
+ val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault()
+
+ return sourceManager.getCatalogueSources()
+ .filter { it.lang in languages }
+ .filterNot { it is LoginSource && !it.isLogged() }
+ .filterNot { it.id.toString() in hiddenCatalogues }
+ .sortedBy { "(${it.lang}) ${it.name}" }
+ }
+
+ /**
+ * Initiates a search for mnaga per catalogue.
+ *
+ * @param query query on which to search.
+ */
+ fun search(query: String) {
+ // Return if there's nothing to do
+ if (this.query == query) return
+
+ // Update query
+ this.query = query
+
+ // Create image fetch subscription
+ initializeFetchImageSubscription()
+
+ // Create items with the initial state
+ val initialItems = sources.map { CatalogueSearchItem(it, null) }
+ var items = initialItems
+
+ fetchSourcesSubscription?.unsubscribe()
+ fetchSourcesSubscription = Observable.from(sources)
+ .flatMap({ source ->
+ source.fetchSearchManga(1, query, FilterList())
+ .subscribeOn(Schedulers.io())
+ .onExceptionResumeNext(Observable.empty()) // Ignore timeouts.
+ .map { it.mangas.take(10) } // Get at most 10 manga from search result.
+ .map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
+ .doOnNext { fetchImage(it, source) } // Load manga covers.
+ .map { CatalogueSearchItem(source, it.map { CatalogueSearchCardItem(it) }) }
+ }, 5)
+ .observeOn(AndroidSchedulers.mainThread())
+ // Update matching source with the obtained results
+ .map { result ->
+ items.map { item -> if (item.source == result.source) result else item }
+ }
+ // Update current state
+ .doOnNext { items = it }
+ // Deliver initial state
+ .startWith(initialItems)
+ .subscribeLatestCache({ view, manga ->
+ view.setItems(manga)
+ }, { _, error ->
+ Timber.e(error)
+ })
+ }
+
+ /**
+ * Initialize a list of manga.
+ *
+ * @param manga the list of manga to initialize.
+ */
+ private fun fetchImage(manga: List, source: Source) {
+ fetchImageSubject.onNext(Pair(manga, source))
+ }
+
+ /**
+ * Subscribes to the initializer of manga details and updates the view if needed.
+ */
+ private fun initializeFetchImageSubscription() {
+ fetchImageSubscription?.unsubscribe()
+ fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io())
+ .flatMap {
+ val source = it.second
+ Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized }
+ .map { Pair(it, source) }
+ .concatMap { getMangaDetailsObservable(it.first, it.second) }
+ .map { Pair(source as CatalogueSource, it) }
+
+ }
+
+ .onBackpressureBuffer()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({ (source, manga) ->
+ @Suppress("DEPRECATION")
+ view?.onMangaInitialized(source, manga)
+ }, { error ->
+ Timber.e(error)
+ })
+ }
+
+ /**
+ * Returns an observable of manga that initializes the given manga.
+ *
+ * @param manga the manga to initialize.
+ * @return an observable of the manga to initialize
+ */
+ private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable {
+ return source.fetchMangaDetails(manga)
+ .flatMap { networkManga ->
+ manga.copyFrom(networkManga)
+ manga.initialized = true
+ db.insertManga(manga).executeAsBlocking()
+ Observable.just(manga)
+ }
+ .onErrorResumeNext { Observable.just(manga) }
+ }
+
+ /**
+ * Returns a manga from the database for the given manga from network. It creates a new entry
+ * if the manga is not yet in the database.
+ *
+ * @param sManga the manga from the source.
+ * @return a manga from the database.
+ */
+ private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
+ var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
+ if (localManga == null) {
+ val newManga = Manga.create(sManga.url, sManga.title, sourceId)
+ newManga.copyFrom(sManga)
+ val result = db.insertManga(newManga).executeAsBlocking()
+ newManga.id = result.insertedId()
+ localManga = newManga
+ }
+ return localManga
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt
new file mode 100644
index 000000000..d2e15169c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt
@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.getResourceColor
+
+/**
+ * Adapter that holds the catalogue cards.
+ *
+ * @param controller instance of [CatalogueMainController].
+ */
+class CatalogueMainAdapter(val controller: CatalogueMainController) :
+ FlexibleAdapter>(null, controller, true) {
+
+ val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
+
+ init {
+ setDisplayHeadersAtStartUp(true)
+ }
+
+ /**
+ * Listener for browse item clicks.
+ */
+ val browseClickListener: OnBrowseClickListener = controller
+
+ /**
+ * Listener for latest item clicks.
+ */
+ val latestClickListener: OnLatestClickListener = controller
+
+ /**
+ * Listener which should be called when user clicks browse.
+ * Note: Should only be handled by [CatalogueMainController]
+ */
+ interface OnBrowseClickListener {
+ fun onBrowseClick(position: Int)
+ }
+
+ /**
+ * Listener which should be called when user clicks latest.
+ * Note: Should only be handled by [CatalogueMainController]
+ */
+ interface OnLatestClickListener {
+ fun onLatestClick(position: Int)
+ }
+}
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt
new file mode 100644
index 000000000..76ea5e513
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt
@@ -0,0 +1,238 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.SearchView
+import android.view.*
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
+import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
+import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
+import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
+import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
+import kotlinx.android.synthetic.main.catalogue_main_controller.view.*
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+/**
+ * This controller shows and manages the different catalogues enabled by the user.
+ * This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter]
+ * [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
+ * [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click.
+ * [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click
+ */
+class CatalogueMainController : NucleusController(),
+ SourceLoginDialog.Listener,
+ FlexibleAdapter.OnItemClickListener,
+ CatalogueMainAdapter.OnBrowseClickListener,
+ CatalogueMainAdapter.OnLatestClickListener {
+
+ /**
+ * Application preferences.
+ */
+ private val preferences: PreferencesHelper = Injekt.get()
+
+ /**
+ * Adapter containing sources.
+ */
+ private var adapter : CatalogueMainAdapter? = null
+
+ /**
+ * Called when controller is initialized.
+ */
+ init {
+ // Enable the option menu
+ setHasOptionsMenu(true)
+ }
+
+ /**
+ * Set the title of controller.
+ *
+ * @return title.
+ */
+ override fun getTitle(): String? {
+ return applicationContext?.getString(R.string.label_catalogues)
+ }
+
+ /**
+ * Create the [CatalogueMainPresenter] used in controller.
+ *
+ * @return instance of [CatalogueMainPresenter]
+ */
+ override fun createPresenter(): CatalogueMainPresenter {
+ return CatalogueMainPresenter()
+ }
+
+ /**
+ * Initiate the view with [R.layout.catalogue_main_controller].
+ *
+ * @param inflater used to load the layout xml.
+ * @param container containing parent views.
+ * @return inflated view.
+ */
+ override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+ return inflater.inflate(R.layout.catalogue_main_controller, container, false)
+ }
+
+ /**
+ * Called when the view is created
+ *
+ * @param view view of controller
+ * @param savedViewState information from previous state.
+ */
+ override fun onViewCreated(view: View, savedViewState: Bundle?) {
+ super.onViewCreated(view, savedViewState)
+
+ adapter = CatalogueMainAdapter(this)
+
+ with(view) {
+ // Create recycler and set adapter.
+ recycler.layoutManager = LinearLayoutManager(context)
+ recycler.adapter = adapter
+ recycler.addItemDecoration(SourceDividerItemDecoration(context))
+ }
+ }
+
+ override fun onDestroyView(view: View) {
+ adapter = null
+ super.onDestroyView(view)
+ }
+
+ override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+ super.onChangeStarted(handler, type)
+ if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
+ presenter.updateSources()
+ }
+ }
+
+ /**
+ * Called when login dialog is closed, refreshes the adapter.
+ *
+ * @param source clicked item containing source information.
+ */
+ override fun loginDialogClosed(source: LoginSource) {
+ if (source.isLogged()) {
+ adapter?.clear()
+ presenter.loadSources()
+ }
+ }
+
+ /**
+ * Called when item is clicked
+ */
+ override fun onItemClick(position: Int): Boolean {
+ val item = adapter?.getItem(position) as? SourceItem ?: return false
+ val source = item.source
+ if (source is LoginSource && !source.isLogged()) {
+ val dialog = SourceLoginDialog(source)
+ dialog.targetController = this
+ dialog.showDialog(router)
+ } else {
+ // Open the catalogue view.
+ openCatalogue(source, CatalogueController(source))
+ }
+ return false
+ }
+
+ /**
+ * Called when browse is clicked in [CatalogueMainAdapter]
+ */
+ override fun onBrowseClick(position: Int) {
+ onItemClick(position)
+ }
+
+ /**
+ * Called when latest is clicked in [CatalogueMainAdapter]
+ */
+ override fun onLatestClick(position: Int) {
+ val item = adapter?.getItem(position) as? SourceItem ?: return
+ openCatalogue(item.source, LatestUpdatesController(item.source))
+ }
+
+ /**
+ * Opens a catalogue with the given controller.
+ */
+ private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) {
+ preferences.lastUsedCatalogueSource().set(source.id)
+ router.pushController(RouterTransaction.with(controller)
+ .popChangeHandler(FadeChangeHandler())
+ .pushChangeHandler(FadeChangeHandler()))
+ }
+
+ /**
+ * Adds items to the options menu.
+ *
+ * @param menu menu containing options.
+ * @param inflater used to load the menu xml.
+ */
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ // Inflate menu
+ inflater.inflate(R.menu.catalogue_main, menu)
+
+ // Initialize search option.
+ val searchItem = menu.findItem(R.id.action_search)
+ val searchView = searchItem.actionView as SearchView
+
+ // Change hint to show global search.
+ searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
+
+ // Create query listener which opens the global search view.
+ searchView.queryTextChangeEvents()
+ .filter { it.isSubmitted }
+ .subscribeUntilDestroy {
+ val query = it.queryText().toString()
+ router.pushController((RouterTransaction.with(CatalogueSearchController(query)))
+ .popChangeHandler(FadeChangeHandler())
+ .pushChangeHandler(FadeChangeHandler()))
+ }
+ }
+
+ /**
+ * Called when an option menu item has been selected by the user.
+ *
+ * @param item The selected item.
+ * @return True if this event has been consumed, false if it has not.
+ */
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ // Initialize option to open catalogue settings.
+ R.id.action_settings -> {
+ router.pushController((RouterTransaction.with(SettingsSourcesController()))
+ .popChangeHandler(SettingsSourcesFadeChangeHandler())
+ .pushChangeHandler(FadeChangeHandler()))
+ }
+ else -> return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ /**
+ * Called to update adapter containing sources.
+ */
+ fun setSources(sources: List>) {
+ adapter?.updateDataSet(sources)
+ }
+
+ /**
+ * Called to set the last used catalogue at the top of the view.
+ */
+ fun setLastUsedSource(item: SourceItem?) {
+ adapter?.removeAllScrollableHeaders()
+ if (item != null) {
+ adapter?.addScrollableHeader(item)
+ }
+ }
+
+ class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt
new file mode 100644
index 000000000..a0745a26a
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt
@@ -0,0 +1,104 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.LocalSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+/**
+ * Presenter of [CatalogueMainController]
+ * Function calls should be done from here. UI calls should be done from the controller.
+ *
+ * @param sourceManager manages the different sources.
+ * @param preferences application preferences.
+ */
+class CatalogueMainPresenter(
+ val sourceManager: SourceManager = Injekt.get(),
+ private val preferences: PreferencesHelper = Injekt.get()
+) : BasePresenter() {
+
+ /**
+ * Enabled sources.
+ */
+ var sources = getEnabledSources()
+
+ /**
+ * Subscription for retrieving enabled sources.
+ */
+ private var sourceSubscription: Subscription? = null
+
+ override fun onCreate(savedState: Bundle?) {
+ super.onCreate(savedState)
+
+ // Load enabled and last used sources
+ loadSources()
+ loadLastUsedSource()
+ }
+
+ /**
+ * Unsubscribe and create a new subscription to fetch enabled sources.
+ */
+ fun loadSources() {
+ sourceSubscription?.unsubscribe()
+
+ val map = TreeMap> { d1, d2 ->
+ // Catalogues without a lang defined will be placed at the end
+ when {
+ d1 == "" && d2 != "" -> 1
+ d2 == "" && d1 != "" -> -1
+ else -> d1.compareTo(d2)
+ }
+ }
+ val byLang = sources.groupByTo(map, { it.lang })
+ val sourceItems = byLang.flatMap {
+ val langItem = LangItem(it.key)
+ it.value.map { source -> SourceItem(source, langItem) }
+ }
+
+ sourceSubscription = Observable.just(sourceItems)
+ .subscribeLatestCache(CatalogueMainController::setSources)
+ }
+
+ private fun loadLastUsedSource() {
+ val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
+
+ // Emit the first item immediately but delay subsequent emissions by 500ms.
+ Observable.merge(
+ sharedObs.take(1),
+ sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
+ .distinctUntilChanged()
+ .map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
+ .subscribeLatestCache(CatalogueMainController::setLastUsedSource)
+ }
+
+ fun updateSources() {
+ sources = getEnabledSources()
+ loadSources()
+ }
+
+ /**
+ * Returns a list of enabled sources ordered by language and name.
+ *
+ * @return list containing enabled sources.
+ */
+ private fun getEnabledSources(): List {
+ val languages = preferences.enabledLanguages().getOrDefault()
+ val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
+
+ return sourceManager.getCatalogueSources()
+ .filter { it.lang in languages }
+ .filterNot { it.id.toString() in hiddenCatalogues }
+ .sortedBy { "(${it.lang}) ${it.name}" } +
+ sourceManager.get(LocalSource.ID) as LocalSource
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt
new file mode 100644
index 000000000..02dcea146
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt
@@ -0,0 +1,21 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
+import java.util.*
+
+class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
+
+ fun bind(item: LangItem) {
+ itemView.title.text = when {
+ item.code == "" -> itemView.context.getString(R.string.other_source)
+ else -> {
+ val locale = Locale(item.code)
+ locale.getDisplayName(locale).capitalize()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt
new file mode 100644
index 000000000..815ad7495
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt
@@ -0,0 +1,38 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractHeaderItem
+import eu.kanade.tachiyomi.R
+
+/**
+ * Item that contains the language header.
+ *
+ * @param code The lang code.
+ */
+data class LangItem(val code: String) : AbstractHeaderItem() {
+
+ /**
+ * Returns the layout resource of this item.
+ */
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_main_controller_card
+ }
+
+ /**
+ * Creates a new view holder for this item.
+ */
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LangHolder {
+ return LangHolder(view, adapter)
+ }
+
+ /**
+ * Binds this item to the given view holder.
+ */
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder,
+ position: Int, payloads: List?) {
+
+ holder.bind(this)
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt
new file mode 100644
index 000000000..bb90f5307
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt
@@ -0,0 +1,47 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.support.v7.widget.RecyclerView
+import android.view.View
+
+class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
+
+ private val divider: Drawable
+
+ init {
+ val a = context.obtainStyledAttributes(ATTRS)
+ divider = a.getDrawable(0)
+ a.recycle()
+ }
+
+ override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+ val left = parent.paddingLeft + SourceHolder.margin
+ val right = parent.width - parent.paddingRight - SourceHolder.margin
+
+ val childCount = parent.childCount
+ for (i in 0 until childCount - 1) {
+ val child = parent.getChildAt(i)
+ if (parent.getChildViewHolder(child) is SourceHolder &&
+ parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
+ val params = child.layoutParams as RecyclerView.LayoutParams
+ val top = child.bottom + params.bottomMargin
+ val bottom = top + divider.intrinsicHeight
+
+ divider.setBounds(left, top, right, bottom)
+ divider.draw(c)
+ }
+ }
+ }
+
+ override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
+ state: RecyclerView.State) {
+ outRect.set(0, 0, 0, divider.intrinsicHeight)
+ }
+
+ companion object {
+ private val ATTRS = intArrayOf(android.R.attr.listDivider)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt
new file mode 100644
index 000000000..ddc8914b0
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt
@@ -0,0 +1,107 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.util.dpToPx
+import eu.kanade.tachiyomi.util.getRound
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.visible
+import io.github.mthli.slice.Slice
+import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.*
+
+class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) {
+
+ private val slice = Slice(itemView.card).apply {
+ setColor(adapter.cardBackground)
+ }
+
+ init {
+ itemView.source_browse.setOnClickListener {
+ adapter.browseClickListener.onBrowseClick(adapterPosition)
+ }
+
+ itemView.source_latest.setOnClickListener {
+ adapter.latestClickListener.onLatestClick(adapterPosition)
+ }
+ }
+
+ fun bind(item: SourceItem) {
+ val source = item.source
+ with(itemView) {
+ setCardEdges(item)
+
+ // Set source name
+ title.text = source.name
+
+ // Set circle letter image.
+ post {
+ image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
+ }
+
+ // If source is login, show only login option
+ if (source is LoginSource && !source.isLogged()) {
+ source_browse.setText(R.string.login)
+ source_latest.gone()
+ } else {
+ source_browse.setText(R.string.browse)
+ source_latest.visible()
+ }
+ }
+ }
+
+ private fun setCardEdges(item: SourceItem) {
+ // Position of this item in its header. Defaults to 0 when header is null.
+ var position = 0
+
+ // Number of items in the header of this item. Defaults to 1 when header is null.
+ var count = 1
+
+ if (item.header != null) {
+ val sectionItems = mAdapter.getSectionItems(item.header)
+ position = sectionItems.indexOf(item)
+ count = sectionItems.size
+ }
+
+ when {
+ // Only one item in the card
+ count == 1 -> applySlice(2f, false, false, true, true)
+ // First item of the card
+ position == 0 -> applySlice(2f, false, true, true, false)
+ // Last item of the card
+ position == count - 1 -> applySlice(2f, true, false, false, true)
+ // Middle item
+ else -> applySlice(0f, false, false, false, false)
+ }
+ }
+
+ private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
+ topShadow: Boolean, bottomShadow: Boolean) {
+
+ slice.setRadius(radius)
+ slice.showLeftTopRect(topRect)
+ slice.showRightTopRect(topRect)
+ slice.showLeftBottomRect(bottomRect)
+ slice.showRightBottomRect(bottomRect)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ slice.showTopEdgeShadow(topShadow)
+ slice.showBottomEdgeShadow(bottomShadow)
+ }
+ setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
+ }
+
+ private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
+ val v = itemView.card
+ if (v.layoutParams is ViewGroup.MarginLayoutParams) {
+ val p = v.layoutParams as ViewGroup.MarginLayoutParams
+ p.setMargins(left, top, right, bottom)
+ }
+ }
+
+ companion object {
+ val margin = 8.dpToPx
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt
new file mode 100644
index 000000000..5031b81d9
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt
@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractSectionableItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.CatalogueSource
+
+/**
+ * Item that contains source information.
+ *
+ * @param source Instance of [CatalogueSource] containing source information.
+ * @param header The header for this item.
+ */
+data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) :
+ AbstractSectionableItem(header) {
+
+ /**
+ * Returns the layout resource of this item.
+ */
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_main_controller_card_item
+ }
+
+ /**
+ * Creates a new view holder for this item.
+ */
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
+ return SourceHolder(view, adapter as CatalogueMainAdapter)
+ }
+
+ /**
+ * Binds this item to the given view holder.
+ */
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder,
+ position: Int, payloads: List?) {
+
+ holder.bind(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt
index 907a5a04a..0ad0eb90c 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt
@@ -20,14 +20,14 @@ class CategoryAdapter(controller: CategoryController) :
*/
override fun clearSelection() {
super.clearSelection()
- (0 until itemCount).forEach { getItem(it).isSelected = false }
+ (0 until itemCount).forEach { getItem(it)?.isSelected = false }
}
/**
* Clears the active selections from the model.
*/
fun clearModelSelection() {
- selectedPositions.forEach { getItem(it).isSelected = false }
+ selectedPositions.forEach { getItem(it)?.isSelected = false }
}
/**
@@ -37,7 +37,7 @@ class CategoryAdapter(controller: CategoryController) :
*/
override fun toggleSelection(position: Int) {
super.toggleSelection(position)
- getItem(position).isSelected = isSelected(position)
+ getItem(position)?.isSelected = isSelected(position)
}
interface OnItemReleaseListener {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt
index 81243c50d..87df04333 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.category
import android.os.Bundle
+import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.LinearLayoutManager
@@ -8,11 +9,12 @@ import android.support.v7.widget.RecyclerView
import android.view.*
import com.jakewharton.rxbinding.view.clicks
import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.SelectableAdapter
+import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.toast
-import eu.kanade.tachiyomi.widget.UndoHelper
import kotlinx.android.synthetic.main.categories_controller.view.*
/**
@@ -38,7 +40,7 @@ class CategoryController : NucleusController(),
private var adapter: CategoryAdapter? = null
/**
- * Undo helper for deleting categories.
+ * Undo helper used for restoring a deleted category.
*/
private var undoHelper: UndoHelper? = null
@@ -79,6 +81,7 @@ class CategoryController : NucleusController(),
recycler.setHasFixedSize(true)
recycler.adapter = adapter
adapter?.isHandleDragEnabled = true
+ adapter?.isPermanentDelete = false
fab.clicks().subscribeUntilDestroy {
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
@@ -93,7 +96,8 @@ class CategoryController : NucleusController(),
*/
override fun onDestroyView(view: View) {
super.onDestroyView(view)
- undoHelper?.dismissNow() // confirm categories deletion if required
+ // Manually call callback to delete categories if required
+ undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null
actionMode = null
adapter = null
@@ -106,7 +110,7 @@ class CategoryController : NucleusController(),
*/
fun setCategories(categories: List) {
actionMode?.finish()
- adapter?.updateDataSet(categories.toMutableList())
+ adapter?.updateDataSet(categories)
val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
@@ -126,7 +130,7 @@ class CategoryController : NucleusController(),
// Inflate menu.
mode.menuInflater.inflate(R.menu.category_selection, menu)
// Enable adapter multi selection.
- adapter?.mode = FlexibleAdapter.MODE_MULTI
+ adapter?.mode = SelectableAdapter.Mode.MULTI
return true
}
@@ -161,26 +165,20 @@ class CategoryController : NucleusController(),
when (item.itemId) {
R.id.action_delete -> {
- undoHelper = UndoHelper(adapter, this).apply {
- withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener {
- override fun onPreAction(): Boolean {
- adapter.clearModelSelection()
- return false
- }
+ undoHelper = UndoHelper(adapter, this)
+ undoHelper?.start(adapter.selectedPositions, view!!,
+ R.string.snack_categories_deleted, R.string.action_undo, 3000)
- override fun onPostAction() {
- mode.finish()
- }
- })
- remove(adapter.selectedPositions, view!!,
- R.string.snack_categories_deleted, R.string.action_undo, 3000)
- }
+ mode.finish()
}
R.id.action_edit -> {
// Edit selected category
if (adapter.selectedItemCount == 1) {
val position = adapter.selectedPositions.first()
- editCategory(adapter.getItem(position).category)
+ val category = adapter.getItem(position)?.category
+ if (category != null) {
+ editCategory(category)
+ }
}
}
else -> return false
@@ -195,7 +193,7 @@ class CategoryController : NucleusController(),
*/
override fun onDestroyActionMode(mode: ActionMode) {
// Reset adapter to single selection
- adapter?.mode = FlexibleAdapter.MODE_IDLE
+ adapter?.mode = SelectableAdapter.Mode.IDLE
adapter?.clearSelection()
actionMode = null
}
@@ -260,7 +258,7 @@ class CategoryController : NucleusController(),
*/
override fun onItemReleased(position: Int) {
val adapter = adapter ?: return
- val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category }
+ val categories = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.category }
presenter.reorderCategories(categories)
}
@@ -269,18 +267,21 @@ class CategoryController : NucleusController(),
*
* @param action The action performed.
*/
- override fun onUndoConfirmed(action: Int) {
+ override fun onActionCanceled(action: Int) {
adapter?.restoreDeletedItems()
+ undoHelper = null
}
/**
* Called when the time to restore the items expires.
*
* @param action The action performed.
+ * @param event The event that triggered the action
*/
- override fun onDeleteConfirmed(action: Int) {
+ override fun onActionConfirmed(action: Int, event: Int) {
val adapter = adapter ?: return
presenter.deleteCategories(adapter.deletedItems.map { it.category })
+ undoHelper = null
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
index 906bbb910..0dd3c1fa7 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
@@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.*
/**
@@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
// Update circle letter image.
itemView.post {
- itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase()))
+ itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false))
}
}
- /**
- * Returns circle letter image.
- *
- * @param text The first letter of string.
- */
- private fun getRound(text: String): TextDrawable {
- val size = Math.min(itemView.image.width, itemView.image.height)
- return TextDrawable.builder()
- .beginConfig()
- .width(size)
- .height(size)
- .textColor(Color.WHITE)
- .useFont(Typeface.DEFAULT)
- .endConfig()
- .buildRound(text, ColorGenerator.MATERIAL.getColor(text))
- }
-
/**
* Called when an item is released.
*
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt
index 40c52cd16..7f24ab528 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt
@@ -1,12 +1,10 @@
package eu.kanade.tachiyomi.ui.category
-import android.view.LayoutInflater
-import android.view.ViewGroup
+import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.util.inflate
/**
* Category item for a recycler view.
@@ -28,15 +26,11 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem,
- inflater: LayoutInflater,
- parent: ViewGroup): CategoryHolder {
-
- return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CategoryHolder {
+ return CategoryHolder(view, adapter as CategoryAdapter)
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt
index 4e5b5e288..0577176e9 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt
@@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter() {
downloadQueue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread())
.map { ArrayList(it) }
- .subscribeLatestCache(DownloadController::onNextDownloads, { view, error ->
+ .subscribeLatestCache(DownloadController::onNextDownloads, { _, error ->
Timber.e(error)
})
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
index 730b5e991..072980607 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
@@ -1,19 +1,25 @@
package eu.kanade.tachiyomi.ui.latest_updates
+import android.os.Bundle
import android.support.v4.widget.DrawerLayout
import android.view.Menu
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
/**
- * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
+ * Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
*/
-class LatestUpdatesController : CatalogueController() {
+class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
+
+ constructor(source: CatalogueSource) : this(Bundle().apply {
+ putLong(SOURCE_ID_KEY, source.id)
+ })
override fun createPresenter(): CataloguePresenter {
- return LatestUpdatesPresenter()
+ return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
}
override fun onPrepareOptionsMenu(menu: Menu) {
@@ -30,4 +36,4 @@ class LatestUpdatesController : CatalogueController() {
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
index 924425b62..2e0ea07fe 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
@@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.ui.latest_updates
-import eu.kanade.tachiyomi.source.CatalogueSource
-import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager
@@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/
-class LatestUpdatesPresenter : CataloguePresenter() {
+class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
- override fun getEnabledSources(): List {
- return super.getEnabledSources().filter { it.supportsLatest }
- }
-
- override fun isValidSource(source: Source?): Boolean {
- return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
index cb19c14f9..d99f61198 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
@@ -6,6 +6,7 @@ import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.widget.FrameLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
@@ -103,9 +104,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
this.category = category
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
- FlexibleAdapter.MODE_MULTI
+ SelectableAdapter.Mode.MULTI
} else {
- FlexibleAdapter.MODE_SINGLE
+ SelectableAdapter.Mode.SINGLE
}
subscriptions += controller.searchRelay
@@ -126,7 +127,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
subscriptions.clear()
}
-
override fun onDetachedFromWindow() {
subscriptions.clear()
super.onDetachedFromWindow()
@@ -145,7 +145,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// Update the category with its manga.
adapter.setItems(mangaForCategory)
- if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
+ if (adapter.mode == SelectableAdapter.Mode.MULTI) {
controller.selectedMangas.forEach { manga ->
val position = adapter.indexOf(manga)
if (position != -1 && !adapter.isSelected(position)) {
@@ -165,19 +165,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
private fun onSelectionChanged(event: LibrarySelectionEvent) {
when (event) {
is LibrarySelectionEvent.Selected -> {
- if (adapter.mode != FlexibleAdapter.MODE_MULTI) {
- adapter.mode = FlexibleAdapter.MODE_MULTI
+ if (adapter.mode != SelectableAdapter.Mode.MULTI) {
+ adapter.mode = SelectableAdapter.Mode.MULTI
}
findAndToggleSelection(event.manga)
}
is LibrarySelectionEvent.Unselected -> {
findAndToggleSelection(event.manga)
if (controller.selectedMangas.isEmpty()) {
- adapter.mode = FlexibleAdapter.MODE_SINGLE
+ adapter.mode = SelectableAdapter.Mode.SINGLE
}
}
is LibrarySelectionEvent.Cleared -> {
- adapter.mode = FlexibleAdapter.MODE_SINGLE
+ adapter.mode = SelectableAdapter.Mode.SINGLE
adapter.clearSelection()
}
}
@@ -205,7 +205,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
override fun onItemClick(position: Int): Boolean {
// If the action mode is created and the position is valid, toggle the selection.
val item = adapter.getItem(position) ?: return false
- if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
+ if (adapter.mode == SelectableAdapter.Mode.MULTI) {
toggleSelection(position)
return true
} else {
@@ -244,4 +244,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
controller.setSelection(item.manga, !adapter.isSelected(position))
controller.invalidateActionMode()
}
+
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
index 4eb320353..f775f249b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
@@ -42,6 +42,7 @@ import io.realm.Realm
import io.realm.RealmResults
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.library_controller.view.*
+import kotlinx.android.synthetic.main.main_activity.*
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber
@@ -219,17 +220,14 @@ class LibraryController(
drawer.addDrawerListener(it)
}
navView = view
-
- navView?.post {
- if (isAttached && drawer.isDrawerOpen(navView))
- drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
- }
+ drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView?.onGroupClicked = { group ->
when (group) {
is LibraryNavigationView.FilterGroup -> onFilterChanged()
is LibraryNavigationView.SortGroup -> onSortChanged()
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
+ is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged()
}
}
@@ -316,7 +314,11 @@ class LibraryController(
*/
private fun onFilterChanged() {
presenter.requestFilterUpdate()
- (activity as? AppCompatActivity)?.supportInvalidateOptionsMenu()
+ activity?.invalidateOptionsMenu()
+ }
+
+ private fun onDownloadBadgeChanged(){
+ presenter.requestDownloadBadgesUpdate()
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
index fcf3789e4..429e80e98 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
@@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
-import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
/**
@@ -19,29 +19,39 @@ import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
class LibraryGridHolder(
private val view: View,
private val adapter: FlexibleAdapter<*>
+
) : LibraryHolder(view, adapter) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
- * @param manga the manga to bind.
+ * @param item the manga item to bind.
*/
- override fun onSetValues(manga: Manga) {
+ override fun onSetValues(item: LibraryItem) {
// Update the title of the manga.
- view.title.text = manga.title
+ view.title.text = item.manga.title
// Update the unread count and its visibility.
with(view.unread_text) {
- visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
- text = manga.unread.toString()
+ visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
+ text = item.manga.unread.toString()
+ }
+ // Update the download count and its visibility.
+ with(view.download_text) {
+ visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
+ text = item.downloadCount.toString()
+ }
+ //set local visibility if its local manga
+ with(view.local_text) {
+ visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
// Update the cover.
- Glide.clear(view.thumbnail)
- Glide.with(view.context)
- .load(manga)
- .diskCacheStrategy(DiskCacheStrategy.RESULT)
+ GlideApp.with(view.context).clear(view.thumbnail)
+ GlideApp.with(view.context)
+ .load(item.manga)
+ .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(view.thumbnail)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
index 2359377da..ff29155c4 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.data.database.models.Manga
/**
* Generic class used to hold the displayed data of a manga in the library.
@@ -21,8 +20,8 @@ abstract class LibraryHolder(
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
- * @param manga the manga to bind.
+ * @param item the manga item to bind.
*/
- abstract fun onSetValues(manga: Manga)
+ abstract fun onSetValues(item: LibraryItem)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
index e454dc8b2..0bd2823ef 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
@@ -1,34 +1,38 @@
package eu.kanade.tachiyomi.ui.library
import android.view.Gravity
-import android.view.LayoutInflater
-import android.view.ViewGroup
+import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
+import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFilterable
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.data.database.models.LibraryManga
+import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
-class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFilterable {
- // Temp metadata holder EH
+class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference) :
+ AbstractFlexibleItem(), IFilterable {
+ // Temp metadata holder (EXH)
@Volatile
var hasMetadata: Boolean? = null
+ var downloadCount = -1
+
override fun getLayoutRes(): Int {
- return R.layout.catalogue_grid_item
+ return if (libraryAsList.getOrDefault())
+ R.layout.catalogue_list_item
+ else
+ R.layout.catalogue_grid_item
}
- override fun createViewHolder(adapter: FlexibleAdapter<*>,
- inflater: LayoutInflater,
- parent: ViewGroup): LibraryHolder {
-
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LibraryHolder {
+ val parent = adapter.recyclerView
return if (parent is AutofitRecyclerView) {
- val view = parent.inflate(R.layout.catalogue_grid_item).apply {
+ view.apply {
val coverHeight = parent.itemWidth / 3 * 4
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
gradient.layoutParams = FrameLayout.LayoutParams(
@@ -36,7 +40,6 @@ class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFi
}
LibraryGridHolder(view, adapter)
} else {
- val view = parent.inflate(R.layout.catalogue_list_item)
LibraryListHolder(view, adapter)
}
}
@@ -46,7 +49,7 @@ class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFi
position: Int,
payloads: List?) {
- holder.onSetValues(manga)
+ holder.onSetValues(this)
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
index 6de899532..37a23fe37 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
@@ -1,11 +1,10 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
-import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import jp.wasabeef.glide.transformations.CropCircleTransformation
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.source.LocalSource
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
/**
@@ -27,16 +26,25 @@ class LibraryListHolder(
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
- * @param manga the manga to bind.
+ * @param item the manga item to bind.
*/
- override fun onSetValues(manga: Manga) {
+ override fun onSetValues(item: LibraryItem) {
// Update the title of the manga.
- itemView.title.text = manga.title
+ itemView.title.text = item.manga.title
// Update the unread count and its visibility.
with(itemView.unread_text) {
- visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
- text = manga.unread.toString()
+ visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE
+ text = item.manga.unread.toString()
+ }
+ // Update the download count and its visibility.
+ with(itemView.download_text) {
+ visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE
+ text = "${item.downloadCount}"
+ }
+ //show local text badge if local manga
+ with(itemView.local_text) {
+ visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE
}
// Create thumbnail onclick to simulate long click
@@ -46,14 +54,14 @@ class LibraryListHolder(
}
// Update the cover.
- Glide.clear(itemView.thumbnail)
- Glide.with(itemView.context)
- .load(manga)
- .diskCacheStrategy(DiskCacheStrategy.RESULT)
+ GlideApp.with(itemView.context).clear(itemView.thumbnail)
+ GlideApp.with(itemView.context)
+ .load(item.manga)
+ .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
- .bitmapTransform(CropCircleTransformation(itemView.context))
+ .circleCrop()
.dontAnimate()
.into(itemView.thumbnail)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt
index 6c1d4a040..e79503a7d 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt
@@ -25,7 +25,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
/**
* List of groups shown in the view.
*/
- private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup())
+ private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup())
/**
* Adapter instance.
@@ -117,7 +117,9 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
- override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total)
+ private val source = Item.MultiSort(R.string.manga_info_source_label, this)
+
+ override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source)
override val header = Item.Header(R.string.action_sort)
@@ -133,6 +135,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE
+ source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE
}
override fun onItemClicked(item: Item) {
@@ -153,6 +156,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
lastUpdated -> LibrarySort.LAST_UPDATED
unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL
+ source -> LibrarySort.SOURCE
else -> throw Exception("Unknown sorting")
})
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
@@ -162,6 +166,23 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
}
+ inner class BadgeGroup : Group {
+ private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this)
+ override val header = null
+ override val footer= null
+ override val items = listOf(downloadBadge)
+ override fun initModels() {
+ downloadBadge.checked = preferences.downloadBadge().getOrDefault()
+ }
+
+ override fun onItemClicked(item: Item) {
+ item as Item.CheckboxGroup
+ item.checked = !item.checked
+ preferences.downloadBadge().set((item.checked))
+ adapter.notifyItemChanged(item)
+ }
+ }
+
/**
* Display group, to show the library as a list or a grid.
*/
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
index 7661f218f..44d5f2522 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.library
import android.os.Bundle
-import android.util.Pair
import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.cache.CoverCache
@@ -29,6 +28,16 @@ import java.io.IOException
import java.io.InputStream
import java.util.*
+/**
+ * Class containing library information.
+ */
+private data class Library(val categories: List, val mangaMap: LibraryMap)
+
+/**
+ * Typealias for the library manga, using the category as keys, and list of manga as values.
+ */
+private typealias LibraryMap = Map>
+
/**
* Presenter of [LibraryController].
*/
@@ -53,6 +62,11 @@ class LibraryPresenter(
*/
private val filterTriggerRelay = BehaviorRelay.create(Unit)
+ /**
+ * Relay used to apply the UI update to the last emission of the library.
+ */
+ private val downloadTriggerRelay = BehaviorRelay.create(Unit)
+
/**
* Relay used to apply the selected sorting method to the last emission of the library.
*/
@@ -74,14 +88,15 @@ class LibraryPresenter(
fun subscribeLibrary() {
if (librarySubscription.isNullOrUnsubscribed()) {
librarySubscription = getLibraryObservable()
+ .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()),
+ { lib, _ -> lib.apply { setDownloadCount(mangaMap) } })
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
- { lib, _ -> Pair(lib.first, applyFilters(lib.second)) })
+ { lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) })
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
- { lib, _ -> Pair(lib.first, applySort(lib.second)) })
- .map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) }
+ { lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) })
.observeOn(AndroidSchedulers.mainThread())
- .subscribeLatestCache({ view, pair ->
- view.onNextLibraryUpdate(pair.first, pair.second)
+ .subscribeLatestCache({ view, (categories, mangaMap) ->
+ view.onNextLibraryUpdate(categories, mangaMap)
})
}
}
@@ -91,7 +106,7 @@ class LibraryPresenter(
*
* @param map the map to filter.
*/
- private fun applyFilters(map: Map>): Map> {
+ private fun applyFilters(map: LibraryMap): LibraryMap {
// Cached list of downloaded manga directories given a source id.
val mangaDirsForSource = mutableMapOf>()
@@ -104,31 +119,36 @@ class LibraryPresenter(
val filterCompleted = preferences.filterCompleted().getOrDefault()
- val filterFn: (Manga) -> Boolean = f@ { manga ->
+ val filterFn: (LibraryItem) -> Boolean = f@ { item ->
// Filter out manga without source.
- val source = sourceManager.get(manga.source) ?: return@f false
+ val source = sourceManager.get(item.manga.source) ?: return@f false
// Filter when there isn't unread chapters.
- if (filterUnread && manga.unread == 0) {
+ if (filterUnread && item.manga.unread == 0) {
return@f false
}
- if (filterCompleted && manga.status != SManga.COMPLETED) {
+ if (filterCompleted && item.manga.status != SManga.COMPLETED) {
return@f false
}
// Filter when the download directory doesn't exist or is null.
if (filterDownloaded) {
+ // Don't bother with directory checking if download count has been set.
+ if (item.downloadCount != -1) {
+ return@f item.downloadCount > 0
+ }
+
// Get the directories for the source of the manga.
val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
val sourceDir = downloadManager.findSourceDir(source)
sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
}
- val mangaDirName = downloadManager.getMangaDirName(manga)
+ val mangaDirName = downloadManager.getMangaDirName(item.manga)
val mangaDir = dirsForSource[mangaDirName] ?: return@f false
- val hasDirs = chapterDirectories.getOrPut(manga.id!!) {
+ val hasDirs = chapterDirectories.getOrPut(item.manga.id!!) {
mangaDir.listFiles()?.isNotEmpty() ?: false
}
if (!hasDirs) {
@@ -141,12 +161,57 @@ class LibraryPresenter(
return map.mapValues { entry -> entry.value.filter(filterFn) }
}
+ /**
+ * Sets downloaded chapter count to each manga.
+ *
+ * @param map the map of manga.
+ */
+ private fun setDownloadCount(map: LibraryMap) {
+ if (!preferences.downloadBadge().getOrDefault()) {
+ // Unset download count if the preference is not enabled.
+ for ((_, itemList) in map) {
+ for (item in itemList) {
+ item.downloadCount = -1
+ }
+ }
+ return
+ }
+
+ // Cached list of downloaded manga directories given a source id.
+ val mangaDirsForSource = mutableMapOf>()
+
+ // Cached list of downloaded chapter directories for a manga.
+ val chapterDirectories = mutableMapOf()
+
+ val downloadCountFn: (LibraryItem) -> Int = f@ { item ->
+ val source = sourceManager.get(item.manga.source) ?: return@f 0
+
+ // Get the directories for the source of the manga.
+ val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
+ val sourceDir = downloadManager.findSourceDir(source)
+ sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
+ }
+ val mangaDirName = downloadManager.getMangaDirName(item.manga)
+ val mangaDir = dirsForSource[mangaDirName] ?: return@f 0
+
+ chapterDirectories.getOrPut(item.manga.id!!) {
+ mangaDir.listFiles()?.size ?: 0
+ }
+ }
+
+ for ((_, itemList) in map) {
+ for (item in itemList) {
+ item.downloadCount = downloadCountFn(item)
+ }
+ }
+ }
+
/**
* Applies library sorting to the given map of manga.
*
* @param map the map to sort.
*/
- private fun applySort(map: Map>): Map> {
+ private fun applySort(map: LibraryMap): LibraryMap {
val sortingMode = preferences.librarySortingMode().getOrDefault()
val lastReadManga by lazy {
@@ -158,22 +223,27 @@ class LibraryPresenter(
db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
}
- val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
+ val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
when (sortingMode) {
- LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
+ LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title)
LibrarySort.LAST_READ -> {
// Get index of manga, set equal to list if size unknown.
- val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size
- val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size
+ val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
+ val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size
manga1LastRead.compareTo(manga2LastRead)
}
- LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
- LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread)
+ LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update)
+ LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
LibrarySort.TOTAL -> {
- val manga1TotalChapter = totalChapterManga[manga1.id!!] ?: 0
- val mange2TotalChapter = totalChapterManga[manga2.id!!] ?: 0
+ val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
+ val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
manga1TotalChapter.compareTo(mange2TotalChapter)
}
+ LibrarySort.SOURCE -> {
+ val source1Name = sourceManager.get(i1.manga.source)?.name ?: ""
+ val source2Name = sourceManager.get(i2.manga.source)?.name ?: ""
+ source1Name.compareTo(source2Name)
+ }
else -> throw Exception("Unknown sorting mode")
}
}
@@ -191,7 +261,7 @@ class LibraryPresenter(
*
* @return an observable of the categories and its manga.
*/
- private fun getLibraryObservable(): Observable, Map>>> {
+ private fun getLibraryObservable(): Observable {
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
{ dbCategories, libraryManga ->
val categories = if (libraryManga.containsKey(0))
@@ -200,7 +270,7 @@ class LibraryPresenter(
dbCategories
this.categories = categories
- Pair(categories, libraryManga)
+ Library(categories, libraryManga)
})
}
@@ -219,9 +289,12 @@ class LibraryPresenter(
* @return an observable containing a map with the category id as key and a list of manga as the
* value.
*/
- private fun getLibraryMangasObservable(): Observable