Global Search (#849)

* Global Search

* Cards are now independent of design by use of recycler.

* Added local

* Some attribute fixes + moved onclick to controller.

* Lots of improvements to code

* Reversed some stuff. Thanks API 16

* Code fixes

* Performance improvements

* Moved adapter creation to constructor

* Small changes

* Removed sources settings from settings menu. Added OnChangeListener in catalogue. Made setting icon visible if room.

* bug fix

* Code review part uno

* Code review part uno-2

* Single recycler approach

* Add last source used

* Fix scroll state and some layout issues

* Fix wrong item binding

* Use data class for items

* Calculate item position and count while binding

* Fix background color with slices

* Reuse slices. Fix card background. Flatten constraint layout

* Fix global_search scroll issue

* Store last state with global search

* Minor changes

* Remove catalogue toolbar spinner. Persist catalogue across process restarts

* Save view state of recycler views. Set toolbar title with current query
This commit is contained in:
Bram van de Kerkhof 2017-09-23 13:11:39 +02:00 committed by inorichi
parent 56bde40035
commit 54c8b3ef29
61 changed files with 1852 additions and 262 deletions

View File

@ -191,6 +191,7 @@ dependencies {
compile 'com.afollestad.material-dialogs:core:0.9.4.5' compile 'com.afollestad.material-dialogs:core:0.9.4.5'
compile 'me.zhanghai.android.systemuihelper:library:1.0.0' compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
compile 'com.github.mthli:Slice:v1.2'
// Conductor // Conductor
compile "com.bluelinelabs:conductor:2.1.4" compile "com.bluelinelabs:conductor:2.1.4"

View File

@ -34,7 +34,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
return null return null
} }
private fun setTitle() { fun setTitle() {
var parentController = parentController var parentController = parentController
while (parentController != null) { while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) { if (parentController is BaseController && parentController.getTitle() != null) {

View File

@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter import nucleus.presenter.Presenter
@Suppress("LeakingThis") @Suppress("LeakingThis")
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(), abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
PresenterFactory<P> { PresenterFactory<P> {
private val delegate = NucleusConductorDelegate(this) private val delegate = NucleusConductorDelegate(this)

View File

@ -4,24 +4,20 @@ import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.* import android.support.v7.widget.*
import android.view.* import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import com.jakewharton.rxbinding.widget.itemSelections
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
@ -43,7 +39,7 @@ import java.util.concurrent.TimeUnit
/** /**
* Controller to manage the catalogues available in the app. * Controller to manage the catalogues available in the app.
*/ */
open class CatalogueController(bundle: Bundle? = null) : open class CatalogueController(bundle: Bundle) :
NucleusController<CataloguePresenter>(bundle), NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController, SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
@ -51,6 +47,10 @@ open class CatalogueController(bundle: Bundle? = null) :
FlexibleAdapter.EndlessScrollListener<ProgressItem>, FlexibleAdapter.EndlessScrollListener<ProgressItem>,
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/** /**
* Preferences helper. * Preferences helper.
*/ */
@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private var adapter: FlexibleAdapter<IFlexible<*>>? = null private var adapter: FlexibleAdapter<IFlexible<*>>? = 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. * Snackbar containing an error message when a request fails.
*/ */
@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private var recycler: RecyclerView? = null private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null 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. * Subscription for the search view.
*/ */
private var searchViewSubscription: Subscription? = null private var searchViewSubscription: Subscription? = null
/**
* Subscription for the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null private var numColumnsSubscription: Subscription? = null
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null private var progressItem: ProgressItem? = null
init { init {
@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
} }
override fun getTitle(): String? { override fun getTitle(): String? {
return "" return presenter.source.toString()
} }
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): CataloguePresenter {
return CataloguePresenter() return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
adapter = FlexibleAdapter(null, this) adapter = FlexibleAdapter(null, this)
setupRecycler(view) 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) 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)
view.progress?.visible() view.progress?.visible()
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) super.onDestroyView(view)
activity?.toolbar?.removeView(spinner)
numColumnsSubscription?.unsubscribe() numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null numColumnsSubscription = null
searchViewSubscription?.unsubscribe() searchViewSubscription?.unsubscribe()
searchViewSubscription = null searchViewSubscription = null
adapter = null adapter = null
spinner = null
snack = null snack = null
recycler = null recycler = null
} }
@ -265,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
menu.findItem(R.id.action_search).apply { menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) { if (!query.isBlank()) {
expandActionView() expandActionView()
searchView.setQuery(query, true) searchView.setQuery(query, true)
@ -328,7 +286,7 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private fun searchWithQuery(newQuery: String) { private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing // If text didn't change, do nothing
if (query == newQuery) if (presenter.query == newQuery)
return return
// FIXME dirty fix to restore the toolbar buttons after closing search mode. // FIXME dirty fix to restore the toolbar buttons after closing search mode.
@ -447,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> { fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
presenter.prefs.portraitColumns() preferences.portraitColumns()
else else
presenter.prefs.landscapeColumns() preferences.landscapeColumns()
} }
/** /**
@ -558,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
presenter.updateMangaCategories(manga, categories) presenter.updateMangaCategories(manga, categories)
} }
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
} }

View File

@ -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.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga 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.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.* import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable import rx.Observable
@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
* Presenter of [CatalogueController]. * Presenter of [CatalogueController].
*/ */
open class CataloguePresenter( open class CataloguePresenter(
val sourceManager: SourceManager = Injekt.get(), sourceId: Long,
val db: DatabaseHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get(),
val prefs: PreferencesHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
val coverCache: CoverCache = Injekt.get() private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() { ) : BasePresenter<CatalogueController>() {
/** /**
* Enabled sources. * Selected source.
*/ */
val sources by lazy { getEnabledSources() } val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Active source.
*/
lateinit var source: CatalogueSource
private set
/** /**
* Query from the view. * Query from the view.
@ -106,7 +97,6 @@ open class CataloguePresenter(
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
source = getLastUsedSource()
sourceFilters = source.getFilterList() sourceFilters = source.getFilterList()
if (savedState != null) { if (savedState != null) {
@ -149,9 +139,9 @@ open class CataloguePresenter(
.doOnNext { initializeMangas(it.second) } .doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map(::CatalogueItem) } .map { it.first to it.second.map(::CatalogueItem) }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, pair -> .subscribeReplay({ view, (page, mangas) ->
view.onAddPage(pair.first, pair.second) view.onAddPage(page, mangas)
}, { view, error -> }, { _, error ->
Timber.e(error) Timber.e(error)
}) })
@ -167,7 +157,7 @@ open class CataloguePresenter(
pageSubscription?.let { remove(it) } pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() } pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ view, page -> .subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted. // Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError) }, CatalogueController::onAddPageError)
} }
@ -179,19 +169,6 @@ open class CataloguePresenter(
return pager.hasNextPage 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. * Sets the display mode.
* *
@ -267,50 +244,6 @@ open class CataloguePresenter(
.onErrorResumeNext { Observable.just(manga) } .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<CatalogueSource> {
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. * Adds or removes a manga from the library.
* *
@ -370,13 +303,12 @@ open class CataloguePresenter(
} }
is Filter.Sort -> { is Filter.Sort -> {
val group = SortGroup(it) val group = SortGroup(it)
val subItems = it.values.mapNotNull { val subItems = it.values.map {
SortItem(it, group) SortItem(it, group)
} }
group.subItems = subItems group.subItems = subItems
group group
} }
else -> null
} }
} }
} }
@ -407,7 +339,7 @@ open class CataloguePresenter(
* @param categories the selected categories. * @param categories the selected categories.
* @param manga the manga to move. * @param manga the manga to move.
*/ */
fun moveMangaToCategories(manga: Manga, categories: List<Category>) { private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga)) db.setMangaCategories(mc, listOf(manga))
} }

View File

@ -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<CatalogueSearchItem>(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<Any?>?) {
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<Parcelable>()
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<Parcelable>(key)
if (holderState != null) {
holder.itemView.restoreHierarchyState(holderState)
bundle.remove(key)
}
}
private companion object {
const val HOLDER_BUNDLE_KEY = "holder_bundle"
}
}

View File

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

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
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) {
Glide.clear(itemView.itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
.into(StateImageViewTarget(itemView.itemImage, itemView.progress))
}
}
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.LayoutInflater
import android.view.ViewGroup
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
class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() {
override fun getLayoutRes(): Int {
return R.layout.catalogue_global_search_controller_card_item
}
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
parent: ViewGroup): CatalogueSearchCardHolder {
return CatalogueSearchCardHolder(parent.inflate(layoutRes), adapter as CatalogueSearchCardAdapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder,
position: Int, payloads: List<Any?>?) {
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
}
}

View File

@ -0,0 +1,171 @@
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<CatalogueSearchPresenter>(),
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
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<CatalogueSearchItem>) {
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)
}
}

View File

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

View File

@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.LayoutInflater
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.util.inflate
/**
* Item that contains search result information.
*
* @param source contains information about search result.
*/
class CatalogueSearchItem(val source: CatalogueSource, val results: List<CatalogueSearchCardItem>?)
: AbstractFlexibleItem<CatalogueSearchHolder>() {
/**
* 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(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
parent: ViewGroup): CatalogueSearchHolder {
return CatalogueSearchHolder(parent.inflate(layoutRes), adapter as CatalogueSearchAdapter)
}
/**
* Bind item to view.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder,
position: Int, payloads: List<Any?>?) {
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()
}
}

View File

@ -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<CatalogueSearchController>() {
/**
* 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<Pair<List<Manga>, 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<CatalogueSource> {
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)
.observeOn(Schedulers.io())
.flatMap { source ->
source.fetchSearchManga(1, query, FilterList())
.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) }) }
}
.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<Manga>, 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<Manga> {
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
}
}

View File

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

View File

@ -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<CatalogueMainPresenter>(),
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<IFlexible<*>>) {
adapter?.updateDataSet(sources.toMutableList())
}
/**
* 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)
}
}
private class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}

View File

@ -0,0 +1,97 @@
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<CatalogueMainController>() {
/**
* 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<String, MutableList<CatalogueSource>> { d1, d2 -> 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<CatalogueSource> {
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
}
}

View File

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

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.LayoutInflater
import android.view.ViewGroup
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<LangHolder>() {
/**
* 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(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
parent: ViewGroup): LangHolder {
return LangHolder(inflater.inflate(layoutRes, parent, false), adapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
}

View File

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

View File

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

View File

@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.LayoutInflater
import android.view.ViewGroup
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<SourceHolder, LangItem>(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(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
parent: ViewGroup): SourceHolder {
val view = inflater.inflate(layoutRes, parent, false)
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<Any?>?) {
holder.bind(this)
}
}

View File

@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.* import kotlinx.android.synthetic.main.categories_item.view.*
/** /**
@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
// Update circle letter image. // Update circle letter image.
itemView.post { 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. * Called when an item is released.
* *

View File

@ -1,19 +1,25 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.latest_updates
import android.os.Bundle
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
import android.view.Menu import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter 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 { override fun createPresenter(): CataloguePresenter {
return LatestUpdatesPresenter() return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {

View File

@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.ui.latest_updates 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.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager import eu.kanade.tachiyomi.ui.catalogue.Pager
@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager
/** /**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter. * Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/ */
class LatestUpdatesPresenter : CataloguePresenter() { class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager { override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source) return LatestUpdatesPager(source)
} }
override fun getEnabledSources(): List<CatalogueSource> {
return super.getEnabledSources().filter { it.supportsLatest }
}
override fun isValidSource(source: Source?): Boolean {
return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
}
} }

View File

@ -18,9 +18,8 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
import eu.kanade.tachiyomi.ui.download.DownloadController import eu.kanade.tachiyomi.ui.download.DownloadController
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
@ -84,8 +83,7 @@ class MainActivity : BaseActivity() {
R.id.nav_drawer_library -> setRoot(LibraryController(), id) R.id.nav_drawer_library -> setRoot(LibraryController(), id)
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id) R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id)
R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id)
R.id.nav_drawer_downloads -> { R.id.nav_drawer_downloads -> {
router.pushController(RouterTransaction.with(DownloadController()) router.pushController(RouterTransaction.with(DownloadController())
.pushChangeHandler(FadeChangeHandler()) .pushChangeHandler(FadeChangeHandler())

View File

@ -30,12 +30,6 @@ class SettingsMainController : SettingsController() {
titleRes = R.string.pref_category_downloads titleRes = R.string.pref_category_downloads
onClick { navigateTo(SettingsDownloadController()) } onClick { navigateTo(SettingsDownloadController()) }
} }
preference {
iconRes = R.drawable.ic_language_black_24dp
iconTint = tintColor
titleRes = R.string.pref_category_sources
onClick { navigateTo(SettingsSourcesController()) }
}
preference { preference {
iconRes = R.drawable.ic_sync_black_24dp iconRes = R.drawable.ic_sync_black_24dp
iconTint = tintColor iconTint = tintColor

View File

@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.setting
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.support.v7.preference.PreferenceGroup import android.support.v7.preference.PreferenceGroup
import android.support.v7.preference.PreferenceScreen import android.support.v7.preference.PreferenceScreen
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager

View File

@ -105,7 +105,7 @@ val Context.powerManager: PowerManager
* *
* @param intent intent that contains broadcast information * @param intent intent that contains broadcast information
*/ */
fun Context.sendLocalBroadcast(intent:Intent){ fun Context.sendLocalBroadcast(intent: Intent) {
LocalBroadcastManager.getInstance(this).sendBroadcast(intent) LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
} }

View File

@ -4,9 +4,12 @@ package eu.kanade.tachiyomi.util
import android.graphics.Color import android.graphics.Color
import android.graphics.Point import android.graphics.Point
import android.graphics.Typeface
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
/** /**
* Returns coordinates of view. * Returns coordinates of view.
@ -43,3 +46,21 @@ inline fun View.invisible() {
inline fun View.gone() { inline fun View.gone() {
visibility = View.GONE visibility = View.GONE
} }
/**
* Returns a TextDrawable determined by input
*
* @param text text of [TextDrawable]
* @param random random color
*/
fun View.getRound(text: String, random : Boolean = true): TextDrawable {
val size = Math.min(this.width, this.height)
return TextDrawable.builder()
.beginConfig()
.width(size)
.height(size)
.textColor(Color.WHITE)
.useFont(Typeface.DEFAULT)
.endConfig()
.buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text))
}

View File

@ -18,6 +18,4 @@
</item> </item>
</selector> </selector>
</item> </item>
</ripple> </ripple>

View File

@ -18,6 +18,4 @@
</item> </item>
</selector> </selector>
</item> </item>
</ripple> </ripple>

View File

@ -18,6 +18,4 @@
</item> </item>
</selector> </selector>
</item> </item>
</ripple> </ripple>

View File

@ -0,0 +1,6 @@
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:id="@android:id/mask">
<color android:color="@android:color/white" />
</item>
</ripple>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="112dp"
android:height="112dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

View File

@ -1,13 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--<selector android:exitFadeDuration="@android:integer/config_longAnimTime"-->
<!--xmlns:android="http://schemas.android.com/apk/res/android">-->
<!--<item android:state_focused="true" android:drawable="?attr/colorAccent"/>-->
<!--<item android:state_pressed="true" android:drawable="?attr/colorAccent"/>-->
<!--<item android:state_activated="true" android:drawable="?attr/colorAccent"/>-->
<!--<item android:drawable="?android:attr/colorBackground"/>-->
<!--</selector>-->
<selector android:exitFadeDuration="@android:integer/config_longAnimTime" <selector android:exitFadeDuration="@android:integer/config_longAnimTime"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">

View File

@ -1,13 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--<selector android:exitFadeDuration="@android:integer/config_longAnimTime"-->
<!--xmlns:android="http://schemas.android.com/apk/res/android">-->
<!--<item android:state_focused="true" android:drawable="?attr/colorAccent"/>-->
<!--<item android:state_pressed="true" android:drawable="?attr/colorAccent"/>-->
<!--<item android:state_activated="true" android:drawable="?attr/colorAccent"/>-->
<!--<item android:drawable="?android:attr/colorBackground"/>-->
<!--</selector>-->
<selector xmlns:android="http://schemas.android.com/apk/res/android" <selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_longAnimTime"> android:exitFadeDuration="@android:integer/config_longAnimTime">

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_longAnimTime">
<item android:drawable="@color/rippleColorLight" android:state_focused="true"/>
<item android:drawable="@color/rippleColorLight" android:state_pressed="true"/>
<item android:drawable="@color/rippleColorLight" android:state_activated="true"/>
<item android:drawable="@android:color/transparent"/>
</selector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke
android:width="1dp"
android:color="?attr/colorAccent" />
<solid android:color="?attr/cardBackgroundColor" />
<padding
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<corners android:radius="5dp" />
</shape>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
android:paddingTop="4dp"
tools:listitem="@layout/catalogue_global_search_controller_card" />
</FrameLayout>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
app:layout_constraintBottom_toTopOf="@+id/source_card"
app:layout_constraintHeight_default="wrap"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<android.support.v7.widget.CardView
android:id="@+id/source_card"
style="@style/Theme.Widget.CardView.Item"
android:layout_width="0dp"
android:layout_height="0dp"
android:minHeight="144dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintStart_toStartOf="parent">
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<android.support.constraint.ConstraintLayout
android:id="@+id/nothing_found"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone">
<ImageView
android:id="@+id/nothing_found_icon"
android:layout_width="112dp"
android:layout_height="112dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/nothing_found_text"
style="@style/TextAppearance.Regular.Caption.Hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:paddingBottom="8dp"
android:text="@string/no_results"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/nothing_found_icon" />
</android.support.constraint.ConstraintLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingEnd="4dp"
android:paddingStart="4dp"
tools:listitem="@layout/catalogue_global_search_controller_card_item" />
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectable_list_drawable"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingEnd="4dp"
android:paddingStart="4dp"
android:paddingTop="8dp">
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleSmall"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintHeight_default="wrap"
app:layout_constraintWidth_default="wrap"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/itemImage"
android:layout_width="112dp"
android:layout_height="112dp"
android:paddingBottom="8dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tvTitle"
style="@style/TextAppearance.Regular.Caption"
android:layout_width="104dp"
android:layout_height="0dp"
android:layout_marginTop="0dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
app:layout_constraintHeight_default="wrap"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/itemImage"
tools:text="Sample title" />
</android.support.constraint.ConstraintLayout>

View File

@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectable_library_drawable"> android:background="?selectable_library_drawable">
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -14,8 +14,7 @@
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:paddingLeft="@dimen/material_component_lists_icon_left_padding" android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
android:paddingRight="0dp" android:paddingRight="0dp"
android:paddingStart="@dimen/material_component_lists_icon_left_padding" android:paddingStart="@dimen/material_component_lists_icon_left_padding"/>
tools:src="@drawable/icon"/>
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/catalogue_main_controller_card" />
</FrameLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingLeft="@dimen/material_component_text_fields_padding_above_and_below_label"
tools:text="Title" />
</FrameLayout>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="@dimen/material_component_lists_two_line_height"
android:background="?attr/selectable_list_drawable">
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="56dp"
android:clickable="true"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:paddingRight="0dp"
android:paddingEnd="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
tools:src="@mipmap/ic_launcher_round"/>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingLeft="16dp"
android:paddingStart="16dp"
android:paddingRight="8dp"
android:paddingEnd="8dp"
android:ellipsize="end"
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@+id/source_latest"
tools:text="Source title"/>
<TextView
android:id="@+id/source_latest"
style="@style/TextAppearance.Medium.Button"
android:background="@drawable/list_item_selector_trans"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/latest"
android:padding="@dimen/material_component_dialogs_padding_around_buttons"
app:layout_constraintRight_toLeftOf="@+id/source_browse"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/source_browse"
style="@style/TextAppearance.Medium.Button"
android:background="@drawable/list_item_selector_trans"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/browse"
android:padding="@dimen/material_component_dialogs_padding_around_buttons"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
</FrameLayout>

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/catalogue_grid" android:id="@+id/catalogue_grid"
style="@style/Theme.Widget.GridView" style="@style/Theme.Widget.GridView.Catalogue"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:columnWidth="140dp" android:columnWidth="140dp"

View File

@ -15,7 +15,8 @@
android:paddingLeft="@dimen/material_component_lists_icon_left_padding" android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
android:paddingStart="@dimen/material_component_lists_icon_left_padding" android:paddingStart="@dimen/material_component_lists_icon_left_padding"
android:paddingRight="0dp" android:paddingRight="0dp"
android:paddingEnd="0dp"/> android:paddingEnd="0dp"
tools:src="@mipmap/ic_launcher_round"/>
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/library_grid" android:id="@+id/library_grid"
style="@style/Theme.Widget.GridView" style="@style/Theme.Widget.GridView.Catalogue"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:columnWidth="140dp" android:columnWidth="140dp"

View File

@ -0,0 +1,16 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".CatalogueListActivity">
<item
android:id="@+id/action_search"
android:title="@string/action_search"
android:icon="@drawable/ic_search_white_24dp"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="android.support.v7.widget.SearchView"/>
<item android:id="@+id/action_settings"
android:title="@string/pref_category_sources"
android:icon="@drawable/ic_settings_white_24dp"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -0,0 +1,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".CatalogueListActivity">
<item
android:id="@+id/action_search"
android:title="@string/action_search"
android:icon="@drawable/ic_search_white_24dp"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="android.support.v7.widget.SearchView"/>
</menu>

View File

@ -34,6 +34,7 @@
<string name="action_sort_last_read">Last read</string> <string name="action_sort_last_read">Last read</string>
<string name="action_sort_last_updated">Last updated</string> <string name="action_sort_last_updated">Last updated</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_global_search">Global search</string>
<string name="action_select_all">Select all</string> <string name="action_select_all">Select all</string>
<string name="action_mark_as_read">Mark as read</string> <string name="action_mark_as_read">Mark as read</string>
<string name="action_mark_as_unread">Mark as unread</string> <string name="action_mark_as_unread">Mark as unread</string>
@ -85,6 +86,8 @@
<string name="action_open_log">Open log</string> <string name="action_open_log">Open log</string>
<string name="action_create">Create</string> <string name="action_create">Create</string>
<string name="action_restore">Restore</string> <string name="action_restore">Restore</string>
<string name="action_open">Open</string>
<string name="action_login">Login</string>
<!-- Operations --> <!-- Operations -->
<string name="deleting">Deleting…</string> <string name="deleting">Deleting…</string>
@ -276,8 +279,13 @@
<string name="no_valid_sources">Please enable at least one valid source</string> <string name="no_valid_sources">Please enable at least one valid source</string>
<string name="no_more_results">No more results</string> <string name="no_more_results">No more results</string>
<string name="local_source">Local manga</string> <string name="local_source">Local manga</string>
<string name="other_source">Other</string>
<string name="invalid_combination">Default can\'t be selected with other categories</string> <string name="invalid_combination">Default can\'t be selected with other categories</string>
<string name="added_to_library">The manga has been added to your library</string> <string name="added_to_library">The manga has been added to your library</string>
<string name="action_global_search_hint">Global search…</string>
<string name="no_results">No results found!</string>
<string name="latest">Latest</string>
<string name="browse">Browse</string>
<!-- Manga activity --> <!-- Manga activity -->
<string name="manga_not_in_db">This manga was removed from the database!</string> <string name="manga_not_in_db">This manga was removed from the database!</string>
@ -430,5 +438,4 @@
<string name="download_notifier_text_only_wifi">No wifi connection available</string> <string name="download_notifier_text_only_wifi">No wifi connection available</string>
<string name="download_notifier_no_network">No network connection available</string> <string name="download_notifier_no_network">No network connection available</string>
<string name="download_notifier_download_paused">Download paused</string> <string name="download_notifier_download_paused">Download paused</string>
</resources> </resources>

View File

@ -4,7 +4,7 @@
<!--========--> <!--========-->
<!--Toolbars--> <!--Toolbars-->
<!--========--> <!--========-->
<style name="Theme.ActionBar" parent="@style/ThemeOverlay.AppCompat.ActionBar"/> <style name="Theme.ActionBar" parent="@style/ThemeOverlay.AppCompat.ActionBar" />
<style name="Theme.ActionBar.Light" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <style name="Theme.ActionBar.Light" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item> <item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
@ -13,12 +13,12 @@
<!--====--> <!--====-->
<!--Tabs--> <!--Tabs-->
<!--====--> <!--====-->
<style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/> <style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<!--===========--> <!--===========-->
<!--AlertDialog--> <!--AlertDialog-->
<!--===========--> <!--===========-->
<style name="Theme.AlertDialog"/> <style name="Theme.AlertDialog" />
<style name="Theme.AlertDialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert"> <style name="Theme.AlertDialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item> <item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
@ -35,7 +35,7 @@
<!--==============--> <!--==============-->
<!--NavigationView--> <!--NavigationView-->
<!--==============--> <!--==============-->
<style name="Theme.Widget.NavigationView"/> <style name="Theme.Widget.NavigationView" />
<style name="Theme.Widget.NavigationView.Dark"> <style name="Theme.Widget.NavigationView.Dark">
<item name="colorControlHighlight">@color/md_grey_900</item> <item name="colorControlHighlight">@color/md_grey_900</item>
@ -85,6 +85,10 @@
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
</style> </style>
<style name="TextAppearance.Regular.SubHeading.Upper">
<item name="android:textAllCaps">true</item>
</style>
<style name="TextAppearance.Regular.SubHeading.Secondary"> <style name="TextAppearance.Regular.SubHeading.Secondary">
<item name="android:textColor">?android:attr/textColorSecondary</item> <item name="android:textColor">?android:attr/textColorSecondary</item>
</style> </style>
@ -105,6 +109,10 @@
<item name="android:textSize">20sp</item> <item name="android:textSize">20sp</item>
</style> </style>
<style name="TextAppearance.Medium.Title.Upper">
<item name="android:textAllCaps">true</item>
</style>
<style name="TextAppearance.Medium.Title.Secondary"> <style name="TextAppearance.Medium.Title.Secondary">
<item name="android:textColor">?android:attr/textColorSecondary</item> <item name="android:textColor">?android:attr/textColorSecondary</item>
</style> </style>
@ -130,7 +138,7 @@
<!--=======--> <!--=======-->
<!--Widgets--> <!--Widgets-->
<!--=======--> <!--=======-->
<style name="Theme.Widget"/> <style name="Theme.Widget" />
<style name="Theme.Widget.FAB"> <style name="Theme.Widget.FAB">
<item name="android:layout_height">@dimen/fab_size</item> <item name="android:layout_height">@dimen/fab_size</item>
@ -147,10 +155,16 @@
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:padding">@dimen/material_component_cards_top_and_bottom_padding</item> <item name="android:padding">@dimen/material_component_cards_top_and_bottom_padding</item>
<item name="android:layout_marginTop">@dimen/material_component_cards_space_between_cards</item> <item name="android:layout_marginTop">@dimen/material_component_cards_space_between_cards
<item name="android:layout_marginBottom">@dimen/material_component_cards_space_between_cards</item> </item>
<item name="android:layout_marginStart">@dimen/material_component_cards_space_between_cards</item> <item name="android:layout_marginBottom">
<item name="android:layout_marginEnd">@dimen/material_component_cards_space_between_cards</item> @dimen/material_component_cards_space_between_cards
</item>
<item name="android:layout_marginStart">
@dimen/material_component_cards_space_between_cards
</item>
<item name="android:layout_marginEnd">@dimen/material_component_cards_space_between_cards
</item>
<item name="cardBackgroundColor">?attr/background_card</item> <item name="cardBackgroundColor">?attr/background_card</item>
<item name="cardElevation">2dp</item> <item name="cardElevation">2dp</item>
</style> </style>
@ -161,21 +175,24 @@
</style> </style>
<style name="Theme.Widget.GridView"> <style name="Theme.Widget.GridView">
<item name="android:smoothScrollbar">true</item>
<item name="android:numColumns">auto_fit</item>
<item name="android:stretchMode">columnWidth</item>
<item name="android:scrollbarStyle">outsideOverlay</item>
</style>
<style name="Theme.Widget.GridView.Catalogue">
<item name="android:padding">5dp</item> <item name="android:padding">5dp</item>
<item name="android:clipToPadding">false</item>
<item name="android:gravity">top|left</item> <item name="android:gravity">top|left</item>
<item name="android:smoothScrollbar">true</item> <item name="android:smoothScrollbar">true</item>
<item name="android:cacheColorHint">?android:attr/textColorHint</item> <item name="android:cacheColorHint">?android:attr/textColorHint</item>
<item name="android:fastScrollEnabled">true</item> <item name="android:fastScrollEnabled">true</item>
<item name="android:horizontalSpacing">0dp</item> <item name="android:horizontalSpacing">0dp</item>
<item name="android:verticalSpacing">0dp</item> <item name="android:verticalSpacing">0dp</item>
<item name="android:numColumns">auto_fit</item>
<item name="android:stretchMode">columnWidth</item>
<item name="android:scrollbarStyle">outsideOverlay</item>
</style> </style>
<style name="Theme.Widget.CheckBox"/> <style name="Theme.Widget.CheckBox" />
<style name="Theme.Widget.CheckBox.Light" parent="TextAppearance.Regular.Body1.Light"> <style name="Theme.Widget.CheckBox.Light" parent="TextAppearance.Regular.Body1.Light">
<item name="buttonTint">@color/md_white_1000</item> <item name="buttonTint">@color/md_white_1000</item>
@ -212,8 +229,7 @@
<item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item> <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
</style> </style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"> <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"></style>
</style>
<style name="reader_settings_popup_animation"> <style name="reader_settings_popup_animation">
<item name="android:windowEnterAnimation">@anim/enter_from_right</item> <item name="android:windowEnterAnimation">@anim/enter_from_right</item>
@ -226,5 +242,4 @@
</style> </style>
</resources> </resources>