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:
parent
56bde40035
commit
54c8b3ef29
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
navView?.setFilters(presenter.filterItems)
|
||||
|
||||
view.progress?.visible()
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
activity?.toolbar?.removeView(spinner)
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
numColumnsSubscription = null
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = null
|
||||
adapter = null
|
||||
spinner = null
|
||||
snack = null
|
||||
recycler = null
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -105,7 +105,7 @@ val Context.powerManager: PowerManager
|
||||
*
|
||||
* @param intent intent that contains broadcast information
|
||||
*/
|
||||
fun Context.sendLocalBroadcast(intent:Intent){
|
||||
fun Context.sendLocalBroadcast(intent: Intent) {
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
@ -18,6 +18,4 @@
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
|
||||
|
||||
</ripple>
|
@ -18,6 +18,4 @@
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
|
||||
|
||||
</ripple>
|
@ -18,6 +18,4 @@
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
|
||||
|
||||
</ripple>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/rippleColorDark">
|
||||
android:color="@color/rippleColorDark">
|
||||
<item>
|
||||
<selector>
|
||||
<item android:state_selected="true">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/rippleColorDark">
|
||||
android:color="@color/rippleColorDark">
|
||||
<item>
|
||||
<selector>
|
||||
<item android:state_selected="true">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/rippleColorLight">
|
||||
android:color="@color/rippleColorLight">
|
||||
<item>
|
||||
<selector>
|
||||
<item android:state_selected="true">
|
||||
|
@ -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>
|
9
app/src/main/res/drawable/ic_search_black_112dp.xml
Normal file
9
app/src/main/res/drawable/ic_search_black_112dp.xml
Normal 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>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
|
||||
<item android:state_focused="true" android:drawable="@color/selectorColorDark"/>
|
||||
<item android:state_pressed="true" android:drawable="@color/selectorColorDark"/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
|
||||
<item android:state_focused="true" android:drawable="@color/selectorColorDark"/>
|
||||
<item android:state_pressed="true" android:drawable="@color/selectorColorDark"/>
|
||||
|
@ -1,15 +1,6 @@
|
||||
<?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">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:state_focused="true" android:drawable="@color/selectorColorLight"/>
|
||||
<item android:state_pressed="true" android:drawable="@color/selectorColorLight"/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:exitFadeDuration="@android:integer/config_longAnimTime">
|
||||
android:exitFadeDuration="@android:integer/config_longAnimTime">
|
||||
|
||||
<item android:drawable="@color/rippleColorDark" android:state_focused="true"/>
|
||||
<item android:drawable="@color/rippleColorDark" android:state_pressed="true"/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:exitFadeDuration="@android:integer/config_longAnimTime">
|
||||
android:exitFadeDuration="@android:integer/config_longAnimTime">
|
||||
|
||||
<item android:drawable="@color/rippleColorDark" android:state_focused="true"/>
|
||||
<item android:drawable="@color/rippleColorDark" android:state_pressed="true"/>
|
||||
|
@ -1,15 +1,6 @@
|
||||
<?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">
|
||||
android:exitFadeDuration="@android:integer/config_longAnimTime">
|
||||
|
||||
<item android:drawable="@color/rippleColorLight" android:state_focused="true"/>
|
||||
<item android:drawable="@color/rippleColorLight" android:state_pressed="true"/>
|
||||
|
10
app/src/main/res/drawable/list_item_selector_trans.xml
Normal file
10
app/src/main/res/drawable/list_item_selector_trans.xml
Normal 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>
|
15
app/src/main/res/drawable/text_button.xml
Normal file
15
app/src/main/res/drawable/text_button.xml
Normal 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>
|
@ -6,12 +6,12 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/catalogue_view"
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/catalogue_view"
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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"
|
||||
|
@ -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"
|
||||
|
14
app/src/main/res/layout/catalogue_main_controller.xml
Normal file
14
app/src/main/res/layout/catalogue_main_controller.xml
Normal 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>
|
18
app/src/main/res/layout/catalogue_main_controller_card.xml
Normal file
18
app/src/main/res/layout/catalogue_main_controller_card.xml
Normal 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>
|
@ -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>
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
16
app/src/main/res/menu/catalogue_main.xml
Normal file
16
app/src/main/res/menu/catalogue_main.xml
Normal 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>
|
11
app/src/main/res/menu/catalogue_new_list.xml
Normal file
11
app/src/main/res/menu/catalogue_new_list.xml
Normal 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>
|
@ -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>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<!--========-->
|
||||
<!--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">
|
||||
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
|
||||
@ -13,12 +13,12 @@
|
||||
<!--====-->
|
||||
<!--Tabs-->
|
||||
<!--====-->
|
||||
<style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
|
||||
<style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
|
||||
<!--===========-->
|
||||
<!--AlertDialog-->
|
||||
<!--===========-->
|
||||
<style name="Theme.AlertDialog"/>
|
||||
<style name="Theme.AlertDialog" />
|
||||
|
||||
<style name="Theme.AlertDialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert">
|
||||
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
|
||||
@ -35,7 +35,7 @@
|
||||
<!--==============-->
|
||||
<!--NavigationView-->
|
||||
<!--==============-->
|
||||
<style name="Theme.Widget.NavigationView"/>
|
||||
<style name="Theme.Widget.NavigationView" />
|
||||
|
||||
<style name="Theme.Widget.NavigationView.Dark">
|
||||
<item name="colorControlHighlight">@color/md_grey_900</item>
|
||||
@ -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>
|
||||
@ -130,7 +138,7 @@
|
||||
<!--=======-->
|
||||
<!--Widgets-->
|
||||
<!--=======-->
|
||||
<style name="Theme.Widget"/>
|
||||
<style name="Theme.Widget" />
|
||||
|
||||
<style name="Theme.Widget.FAB">
|
||||
<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_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,21 +175,24 @@
|
||||
</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>
|
||||
|
||||
|
||||
<style name="Theme.Widget.CheckBox"/>
|
||||
<style name="Theme.Widget.CheckBox" />
|
||||
|
||||
<style name="Theme.Widget.CheckBox.Light" parent="TextAppearance.Regular.Body1.Light">
|
||||
<item name="buttonTint">@color/md_white_1000</item>
|
||||
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user