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 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
compile 'com.github.mthli:Slice:v1.2'
// Conductor
compile "com.bluelinelabs:conductor:2.1.4"

View file

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

View file

@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter
@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> {
private val delegate = NucleusConductorDelegate(this)

View file

@ -4,24 +4,20 @@ import android.content.res.Configuration
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.*
import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import com.jakewharton.rxbinding.widget.itemSelections
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
@ -43,7 +39,7 @@ import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
*/
open class CatalogueController(bundle: Bundle? = null) :
open class CatalogueController(bundle: Bundle) :
NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener,
@ -51,6 +47,10 @@ open class CatalogueController(bundle: Bundle? = null) :
FlexibleAdapter.EndlessScrollListener<ProgressItem>,
ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/**
* Preferences helper.
*/
@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private var adapter: FlexibleAdapter<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.
*/
@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* Query of the search box.
*/
private val query: String
get() = presenter.query
/**
* Selected index of the spinner (selected source).
*/
private var selectedIndex: Int = 0
/**
* Subscription for the search view.
*/
private var searchViewSubscription: Subscription? = null
/**
* Subscription for the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
init {
@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
}
override fun getTitle(): String? {
return ""
return presenter.source.toString()
}
override fun createPresenter(): CataloguePresenter {
return CataloguePresenter()
return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
// Create toolbar spinner
val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
?: activity
val spinnerAdapter = ArrayAdapter(themedContext,
android.R.layout.simple_spinner_item, presenter.sources)
spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item)
val onItemSelected: (Int) -> Unit = { position ->
val source = spinnerAdapter.getItem(position)
if (!presenter.isValidSource(source)) {
spinner?.setSelection(selectedIndex)
activity?.toast(R.string.source_requires_login)
} else if (source != presenter.source) {
selectedIndex = position
showProgressBar()
adapter?.clear()
presenter.setActiveSource(source)
navView?.setFilters(presenter.filterItems)
activity?.invalidateOptionsMenu()
}
}
selectedIndex = presenter.sources.indexOf(presenter.source)
spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter
setSelection(selectedIndex)
itemSelections()
.skip(1)
.filter { it != AdapterView.INVALID_POSITION }
.subscribeUntilDestroy { onItemSelected(it) }
}
activity?.toolbar?.addView(spinner)
view.progress?.visible()
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
activity?.toolbar?.removeView(spinner)
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
spinner = null
snack = null
recycler = null
}
@ -265,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
@ -328,7 +286,7 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
if (query == newQuery)
if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
@ -447,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
presenter.prefs.portraitColumns()
preferences.portraitColumns()
else
presenter.prefs.landscapeColumns()
preferences.landscapeColumns()
}
/**
@ -558,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
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.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable
@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
* Presenter of [CatalogueController].
*/
open class CataloguePresenter(
val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
val prefs: PreferencesHelper = Injekt.get(),
val coverCache: CoverCache = Injekt.get()
sourceId: Long,
sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() {
/**
* Enabled sources.
* Selected source.
*/
val sources by lazy { getEnabledSources() }
/**
* Active source.
*/
lateinit var source: CatalogueSource
private set
val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Query from the view.
@ -106,7 +97,6 @@ open class CataloguePresenter(
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
source = getLastUsedSource()
sourceFilters = source.getFilterList()
if (savedState != null) {
@ -149,9 +139,9 @@ open class CataloguePresenter(
.doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map(::CatalogueItem) }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, pair ->
view.onAddPage(pair.first, pair.second)
}, { view, error ->
.subscribeReplay({ view, (page, mangas) ->
view.onAddPage(page, mangas)
}, { _, error ->
Timber.e(error)
})
@ -167,7 +157,7 @@ open class CataloguePresenter(
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ view, page ->
.subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError)
}
@ -179,19 +169,6 @@ open class CataloguePresenter(
return pager.hasNextPage
}
/**
* Sets the active source and restarts the pager.
*
* @param source the new active source.
*/
fun setActiveSource(source: CatalogueSource) {
prefs.lastUsedCatalogueSource().set(source.id)
this.source = source
sourceFilters = source.getFilterList()
restartPager(query = "", filters = FilterList())
}
/**
* Sets the display mode.
*
@ -267,50 +244,6 @@ open class CataloguePresenter(
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Returns the last used source from preferences or the first valid source.
*
* @return a source.
*/
fun getLastUsedSource(): CatalogueSource {
val id = prefs.lastUsedCatalogueSource().get() ?: -1
val source = sourceManager.get(id)
if (!isValidSource(source) || source !in sources) {
return sources.first { isValidSource(it) }
}
return source as CatalogueSource
}
/**
* Checks if the given source is valid.
*
* @param source the source to check.
* @return true if the source is valid, false otherwise.
*/
open fun isValidSource(source: Source?): Boolean {
if (source == null) return false
if (source is LoginSource) {
return source.isLogged() ||
(prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "")
}
return true
}
/**
* Returns a list of enabled sources ordered by language and name.
*/
open protected fun getEnabledSources(): List<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.
*
@ -370,13 +303,12 @@ open class CataloguePresenter(
}
is Filter.Sort -> {
val group = SortGroup(it)
val subItems = it.values.mapNotNull {
val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
else -> null
}
}
}
@ -407,7 +339,7 @@ open class CataloguePresenter(
* @param categories the selected categories.
* @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) }
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 eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.*
/**
@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
// Update circle letter image.
itemView.post {
itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase()))
itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false))
}
}
/**
* Returns circle letter image.
*
* @param text The first letter of string.
*/
private fun getRound(text: String): TextDrawable {
val size = Math.min(itemView.image.width, itemView.image.height)
return TextDrawable.builder()
.beginConfig()
.width(size)
.height(size)
.textColor(Color.WHITE)
.useFont(Typeface.DEFAULT)
.endConfig()
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
}
/**
* Called when an item is released.
*

View file

@ -1,19 +1,25 @@
package eu.kanade.tachiyomi.ui.latest_updates
import android.os.Bundle
import android.support.v4.widget.DrawerLayout
import android.view.Menu
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
/**
* Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
* Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
*/
class LatestUpdatesController : CatalogueController() {
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
override fun createPresenter(): CataloguePresenter {
return LatestUpdatesPresenter()
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
}
override fun onPrepareOptionsMenu(menu: Menu) {

View file

@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.ui.latest_updates
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager
@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/
class LatestUpdatesPresenter : CataloguePresenter() {
class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
override fun getEnabledSources(): List<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.SecondaryDrawerController
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.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController
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_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id)
R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id)
R.id.nav_drawer_downloads -> {
router.pushController(RouterTransaction.with(DownloadController())
.pushChangeHandler(FadeChangeHandler())

View file

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

View file

@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.setting
import android.graphics.drawable.Drawable
import android.support.v7.preference.PreferenceGroup
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.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager

View file

@ -4,9 +4,12 @@ package eu.kanade.tachiyomi.util
import android.graphics.Color
import android.graphics.Point
import android.graphics.Typeface
import android.support.design.widget.Snackbar
import android.view.View
import android.widget.TextView
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
/**
* Returns coordinates of view.
@ -43,3 +46,21 @@ inline fun View.invisible() {
inline fun 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>
</selector>
</item>
</ripple>

View file

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

View file

@ -18,6 +18,4 @@
</item>
</selector>
</item>
</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"?>
<!--<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"
xmlns:android="http://schemas.android.com/apk/res/android">

View file

@ -1,13 +1,4 @@
<?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"
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectable_library_drawable">
android:background="?selectable_library_drawable">
<FrameLayout
android:layout_width="wrap_content"

View file

@ -14,8 +14,7 @@
android:paddingEnd="0dp"
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
android:paddingRight="0dp"
android:paddingStart="@dimen/material_component_lists_icon_left_padding"
tools:src="@drawable/icon"/>
android:paddingStart="@dimen/material_component_lists_icon_left_padding"/>
<RelativeLayout
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:tools="http://schemas.android.com/tools"
android:id="@+id/catalogue_grid"
style="@style/Theme.Widget.GridView"
style="@style/Theme.Widget.GridView.Catalogue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnWidth="140dp"

View file

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

View file

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/library_grid"
style="@style/Theme.Widget.GridView"
style="@style/Theme.Widget.GridView.Catalogue"
android:layout_width="match_parent"
android:layout_height="match_parent"
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_updated">Last updated</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_mark_as_read">Mark as read</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_create">Create</string>
<string name="action_restore">Restore</string>
<string name="action_open">Open</string>
<string name="action_login">Login</string>
<!-- Operations -->
<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_more_results">No more results</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="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 -->
<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_no_network">No network connection available</string>
<string name="download_notifier_download_paused">Download paused</string>
</resources>

View file

@ -85,6 +85,10 @@
<item name="android:textSize">16sp</item>
</style>
<style name="TextAppearance.Regular.SubHeading.Upper">
<item name="android:textAllCaps">true</item>
</style>
<style name="TextAppearance.Regular.SubHeading.Secondary">
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
@ -105,6 +109,10 @@
<item name="android:textSize">20sp</item>
</style>
<style name="TextAppearance.Medium.Title.Upper">
<item name="android:textAllCaps">true</item>
</style>
<style name="TextAppearance.Medium.Title.Secondary">
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
@ -147,10 +155,16 @@
<item name="android:layout_width">match_parent</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:layout_marginTop">@dimen/material_component_cards_space_between_cards</item>
<item name="android:layout_marginBottom">@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="android:layout_marginTop">@dimen/material_component_cards_space_between_cards
</item>
<item name="android:layout_marginBottom">
@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="cardElevation">2dp</item>
</style>
@ -161,17 +175,20 @@
</style>
<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:clipToPadding">false</item>
<item name="android:gravity">top|left</item>
<item name="android:smoothScrollbar">true</item>
<item name="android:cacheColorHint">?android:attr/textColorHint</item>
<item name="android:fastScrollEnabled">true</item>
<item name="android:horizontalSpacing">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>
@ -212,8 +229,7 @@
<item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
</style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert">
</style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"></style>
<style name="reader_settings_popup_animation">
<item name="android:windowEnterAnimation">@anim/enter_from_right</item>
@ -226,5 +242,4 @@
</style>
</resources>