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
61 changed files with 1852 additions and 262 deletions
|
@ -191,6 +191,7 @@ dependencies {
|
||||||
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
|
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
|
||||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||||
|
compile 'com.github.mthli:Slice:v1.2'
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
compile "com.bluelinelabs:conductor:2.1.4"
|
compile "com.bluelinelabs:conductor:2.1.4"
|
||||||
|
|
|
@ -34,7 +34,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTitle() {
|
fun setTitle() {
|
||||||
var parentController = parentController
|
var parentController = parentController
|
||||||
while (parentController != null) {
|
while (parentController != null) {
|
||||||
if (parentController is BaseController && parentController.getTitle() != null) {
|
if (parentController is BaseController && parentController.getTitle() != null) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
|
||||||
import nucleus.presenter.Presenter
|
import nucleus.presenter.Presenter
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(),
|
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
|
||||||
PresenterFactory<P> {
|
PresenterFactory<P> {
|
||||||
|
|
||||||
private val delegate = NucleusConductorDelegate(this)
|
private val delegate = NucleusConductorDelegate(this)
|
||||||
|
|
|
@ -4,24 +4,20 @@ import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v4.widget.DrawerLayout
|
import android.support.v4.widget.DrawerLayout
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.*
|
import android.support.v7.widget.*
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.Spinner
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||||
import com.f2prateek.rx.preferences.Preference
|
import com.f2prateek.rx.preferences.Preference
|
||||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||||
import com.jakewharton.rxbinding.widget.itemSelections
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||||
|
@ -43,7 +39,7 @@ import java.util.concurrent.TimeUnit
|
||||||
/**
|
/**
|
||||||
* Controller to manage the catalogues available in the app.
|
* Controller to manage the catalogues available in the app.
|
||||||
*/
|
*/
|
||||||
open class CatalogueController(bundle: Bundle? = null) :
|
open class CatalogueController(bundle: Bundle) :
|
||||||
NucleusController<CataloguePresenter>(bundle),
|
NucleusController<CataloguePresenter>(bundle),
|
||||||
SecondaryDrawerController,
|
SecondaryDrawerController,
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
@ -51,6 +47,10 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
FlexibleAdapter.EndlessScrollListener<ProgressItem>,
|
FlexibleAdapter.EndlessScrollListener<ProgressItem>,
|
||||||
ChangeMangaCategoriesDialog.Listener {
|
ChangeMangaCategoriesDialog.Listener {
|
||||||
|
|
||||||
|
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||||
|
putLong(SOURCE_ID_KEY, source.id)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences helper.
|
* Preferences helper.
|
||||||
*/
|
*/
|
||||||
|
@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
*/
|
*/
|
||||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Spinner shown in the toolbar to change the selected source.
|
|
||||||
*/
|
|
||||||
private var spinner: Spinner? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snackbar containing an error message when a request fails.
|
* Snackbar containing an error message when a request fails.
|
||||||
*/
|
*/
|
||||||
|
@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
*/
|
*/
|
||||||
private var recycler: RecyclerView? = null
|
private var recycler: RecyclerView? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawer listener to allow swipe only for closing the drawer.
|
||||||
|
*/
|
||||||
private var drawerListener: DrawerLayout.DrawerListener? = null
|
private var drawerListener: DrawerLayout.DrawerListener? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Query of the search box.
|
|
||||||
*/
|
|
||||||
private val query: String
|
|
||||||
get() = presenter.query
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selected index of the spinner (selected source).
|
|
||||||
*/
|
|
||||||
private var selectedIndex: Int = 0
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for the search view.
|
* Subscription for the search view.
|
||||||
*/
|
*/
|
||||||
private var searchViewSubscription: Subscription? = null
|
private var searchViewSubscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription for the number of manga per row.
|
||||||
|
*/
|
||||||
private var numColumnsSubscription: Subscription? = null
|
private var numColumnsSubscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endless loading item.
|
||||||
|
*/
|
||||||
private var progressItem: ProgressItem? = null
|
private var progressItem: ProgressItem? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
return ""
|
return presenter.source.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): CataloguePresenter {
|
override fun createPresenter(): CataloguePresenter {
|
||||||
return CataloguePresenter()
|
return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
adapter = FlexibleAdapter(null, this)
|
adapter = FlexibleAdapter(null, this)
|
||||||
setupRecycler(view)
|
setupRecycler(view)
|
||||||
|
|
||||||
// Create toolbar spinner
|
|
||||||
val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
|
|
||||||
?: activity
|
|
||||||
|
|
||||||
val spinnerAdapter = ArrayAdapter(themedContext,
|
|
||||||
android.R.layout.simple_spinner_item, presenter.sources)
|
|
||||||
spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item)
|
|
||||||
|
|
||||||
val onItemSelected: (Int) -> Unit = { position ->
|
|
||||||
val source = spinnerAdapter.getItem(position)
|
|
||||||
if (!presenter.isValidSource(source)) {
|
|
||||||
spinner?.setSelection(selectedIndex)
|
|
||||||
activity?.toast(R.string.source_requires_login)
|
|
||||||
} else if (source != presenter.source) {
|
|
||||||
selectedIndex = position
|
|
||||||
showProgressBar()
|
|
||||||
adapter?.clear()
|
|
||||||
presenter.setActiveSource(source)
|
|
||||||
navView?.setFilters(presenter.filterItems)
|
navView?.setFilters(presenter.filterItems)
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedIndex = presenter.sources.indexOf(presenter.source)
|
|
||||||
|
|
||||||
spinner = Spinner(themedContext).apply {
|
|
||||||
adapter = spinnerAdapter
|
|
||||||
setSelection(selectedIndex)
|
|
||||||
itemSelections()
|
|
||||||
.skip(1)
|
|
||||||
.filter { it != AdapterView.INVALID_POSITION }
|
|
||||||
.subscribeUntilDestroy { onItemSelected(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
activity?.toolbar?.addView(spinner)
|
|
||||||
|
|
||||||
view.progress?.visible()
|
view.progress?.visible()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
activity?.toolbar?.removeView(spinner)
|
|
||||||
numColumnsSubscription?.unsubscribe()
|
numColumnsSubscription?.unsubscribe()
|
||||||
numColumnsSubscription = null
|
numColumnsSubscription = null
|
||||||
searchViewSubscription?.unsubscribe()
|
searchViewSubscription?.unsubscribe()
|
||||||
searchViewSubscription = null
|
searchViewSubscription = null
|
||||||
adapter = null
|
adapter = null
|
||||||
spinner = null
|
|
||||||
snack = null
|
snack = null
|
||||||
recycler = null
|
recycler = null
|
||||||
}
|
}
|
||||||
|
@ -265,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
menu.findItem(R.id.action_search).apply {
|
menu.findItem(R.id.action_search).apply {
|
||||||
val searchView = actionView as SearchView
|
val searchView = actionView as SearchView
|
||||||
|
|
||||||
|
val query = presenter.query
|
||||||
if (!query.isBlank()) {
|
if (!query.isBlank()) {
|
||||||
expandActionView()
|
expandActionView()
|
||||||
searchView.setQuery(query, true)
|
searchView.setQuery(query, true)
|
||||||
|
@ -328,7 +286,7 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
*/
|
*/
|
||||||
private fun searchWithQuery(newQuery: String) {
|
private fun searchWithQuery(newQuery: String) {
|
||||||
// If text didn't change, do nothing
|
// If text didn't change, do nothing
|
||||||
if (query == newQuery)
|
if (presenter.query == newQuery)
|
||||||
return
|
return
|
||||||
|
|
||||||
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
|
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
|
||||||
|
@ -447,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
*/
|
*/
|
||||||
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||||
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
|
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
|
||||||
presenter.prefs.portraitColumns()
|
preferences.portraitColumns()
|
||||||
else
|
else
|
||||||
presenter.prefs.landscapeColumns()
|
preferences.landscapeColumns()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -558,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||||
presenter.updateMangaCategories(manga, categories)
|
presenter.updateMangaCategories(manga, categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected companion object {
|
||||||
|
const val SOURCE_ID_KEY = "sourceId"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.filter.*
|
import eu.kanade.tachiyomi.ui.catalogue.filter.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
|
||||||
* Presenter of [CatalogueController].
|
* Presenter of [CatalogueController].
|
||||||
*/
|
*/
|
||||||
open class CataloguePresenter(
|
open class CataloguePresenter(
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
sourceId: Long,
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
sourceManager: SourceManager = Injekt.get(),
|
||||||
val prefs: PreferencesHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
val coverCache: CoverCache = Injekt.get()
|
private val prefs: PreferencesHelper = Injekt.get(),
|
||||||
|
private val coverCache: CoverCache = Injekt.get()
|
||||||
) : BasePresenter<CatalogueController>() {
|
) : BasePresenter<CatalogueController>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enabled sources.
|
* Selected source.
|
||||||
*/
|
*/
|
||||||
val sources by lazy { getEnabledSources() }
|
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||||
|
|
||||||
/**
|
|
||||||
* Active source.
|
|
||||||
*/
|
|
||||||
lateinit var source: CatalogueSource
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query from the view.
|
* Query from the view.
|
||||||
|
@ -106,7 +97,6 @@ open class CataloguePresenter(
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
source = getLastUsedSource()
|
|
||||||
sourceFilters = source.getFilterList()
|
sourceFilters = source.getFilterList()
|
||||||
|
|
||||||
if (savedState != null) {
|
if (savedState != null) {
|
||||||
|
@ -149,9 +139,9 @@ open class CataloguePresenter(
|
||||||
.doOnNext { initializeMangas(it.second) }
|
.doOnNext { initializeMangas(it.second) }
|
||||||
.map { it.first to it.second.map(::CatalogueItem) }
|
.map { it.first to it.second.map(::CatalogueItem) }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeReplay({ view, pair ->
|
.subscribeReplay({ view, (page, mangas) ->
|
||||||
view.onAddPage(pair.first, pair.second)
|
view.onAddPage(page, mangas)
|
||||||
}, { view, error ->
|
}, { _, error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -167,7 +157,7 @@ open class CataloguePresenter(
|
||||||
|
|
||||||
pageSubscription?.let { remove(it) }
|
pageSubscription?.let { remove(it) }
|
||||||
pageSubscription = Observable.defer { pager.requestNext() }
|
pageSubscription = Observable.defer { pager.requestNext() }
|
||||||
.subscribeFirst({ view, page ->
|
.subscribeFirst({ _, _ ->
|
||||||
// Nothing to do when onNext is emitted.
|
// Nothing to do when onNext is emitted.
|
||||||
}, CatalogueController::onAddPageError)
|
}, CatalogueController::onAddPageError)
|
||||||
}
|
}
|
||||||
|
@ -179,19 +169,6 @@ open class CataloguePresenter(
|
||||||
return pager.hasNextPage
|
return pager.hasNextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the active source and restarts the pager.
|
|
||||||
*
|
|
||||||
* @param source the new active source.
|
|
||||||
*/
|
|
||||||
fun setActiveSource(source: CatalogueSource) {
|
|
||||||
prefs.lastUsedCatalogueSource().set(source.id)
|
|
||||||
this.source = source
|
|
||||||
sourceFilters = source.getFilterList()
|
|
||||||
|
|
||||||
restartPager(query = "", filters = FilterList())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the display mode.
|
* Sets the display mode.
|
||||||
*
|
*
|
||||||
|
@ -267,50 +244,6 @@ open class CataloguePresenter(
|
||||||
.onErrorResumeNext { Observable.just(manga) }
|
.onErrorResumeNext { Observable.just(manga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the last used source from preferences or the first valid source.
|
|
||||||
*
|
|
||||||
* @return a source.
|
|
||||||
*/
|
|
||||||
fun getLastUsedSource(): CatalogueSource {
|
|
||||||
val id = prefs.lastUsedCatalogueSource().get() ?: -1
|
|
||||||
val source = sourceManager.get(id)
|
|
||||||
if (!isValidSource(source) || source !in sources) {
|
|
||||||
return sources.first { isValidSource(it) }
|
|
||||||
}
|
|
||||||
return source as CatalogueSource
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given source is valid.
|
|
||||||
*
|
|
||||||
* @param source the source to check.
|
|
||||||
* @return true if the source is valid, false otherwise.
|
|
||||||
*/
|
|
||||||
open fun isValidSource(source: Source?): Boolean {
|
|
||||||
if (source == null) return false
|
|
||||||
|
|
||||||
if (source is LoginSource) {
|
|
||||||
return source.isLogged() ||
|
|
||||||
(prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of enabled sources ordered by language and name.
|
|
||||||
*/
|
|
||||||
open protected fun getEnabledSources(): List<CatalogueSource> {
|
|
||||||
val languages = prefs.enabledLanguages().getOrDefault()
|
|
||||||
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
|
|
||||||
|
|
||||||
return sourceManager.getCatalogueSources()
|
|
||||||
.filter { it.lang in languages }
|
|
||||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
|
||||||
.sortedBy { "(${it.lang}) ${it.name}" } +
|
|
||||||
sourceManager.get(LocalSource.ID) as LocalSource
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or removes a manga from the library.
|
* Adds or removes a manga from the library.
|
||||||
*
|
*
|
||||||
|
@ -370,13 +303,12 @@ open class CataloguePresenter(
|
||||||
}
|
}
|
||||||
is Filter.Sort -> {
|
is Filter.Sort -> {
|
||||||
val group = SortGroup(it)
|
val group = SortGroup(it)
|
||||||
val subItems = it.values.mapNotNull {
|
val subItems = it.values.map {
|
||||||
SortItem(it, group)
|
SortItem(it, group)
|
||||||
}
|
}
|
||||||
group.subItems = subItems
|
group.subItems = subItems
|
||||||
group
|
group
|
||||||
}
|
}
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -407,7 +339,7 @@ open class CataloguePresenter(
|
||||||
* @param categories the selected categories.
|
* @param categories the selected categories.
|
||||||
* @param manga the manga to move.
|
* @param manga the manga to move.
|
||||||
*/
|
*/
|
||||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||||
db.setMangaCategories(mc, listOf(manga))
|
db.setMangaCategories(mc, listOf(manga))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.util.getRound
|
||||||
import kotlinx.android.synthetic.main.categories_item.view.*
|
import kotlinx.android.synthetic.main.categories_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
||||||
|
|
||||||
// Update circle letter image.
|
// Update circle letter image.
|
||||||
itemView.post {
|
itemView.post {
|
||||||
itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase()))
|
itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns circle letter image.
|
|
||||||
*
|
|
||||||
* @param text The first letter of string.
|
|
||||||
*/
|
|
||||||
private fun getRound(text: String): TextDrawable {
|
|
||||||
val size = Math.min(itemView.image.width, itemView.image.height)
|
|
||||||
return TextDrawable.builder()
|
|
||||||
.beginConfig()
|
|
||||||
.width(size)
|
|
||||||
.height(size)
|
|
||||||
.textColor(Color.WHITE)
|
|
||||||
.useFont(Typeface.DEFAULT)
|
|
||||||
.endConfig()
|
|
||||||
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item is released.
|
* Called when an item is released.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
package eu.kanade.tachiyomi.ui.latest_updates
|
package eu.kanade.tachiyomi.ui.latest_updates
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.support.v4.widget.DrawerLayout
|
import android.support.v4.widget.DrawerLayout
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
|
* Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
|
||||||
*/
|
*/
|
||||||
class LatestUpdatesController : CatalogueController() {
|
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
|
||||||
|
|
||||||
|
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||||
|
putLong(SOURCE_ID_KEY, source.id)
|
||||||
|
})
|
||||||
|
|
||||||
override fun createPresenter(): CataloguePresenter {
|
override fun createPresenter(): CataloguePresenter {
|
||||||
return LatestUpdatesPresenter()
|
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.ui.latest_updates
|
package eu.kanade.tachiyomi.ui.latest_updates
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||||
|
@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||||
/**
|
/**
|
||||||
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
|
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
|
||||||
*/
|
*/
|
||||||
class LatestUpdatesPresenter : CataloguePresenter() {
|
class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
|
||||||
|
|
||||||
override fun createPager(query: String, filters: FilterList): Pager {
|
override fun createPager(query: String, filters: FilterList): Pager {
|
||||||
return LatestUpdatesPager(source)
|
return LatestUpdatesPager(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEnabledSources(): List<CatalogueSource> {
|
|
||||||
return super.getEnabledSources().filter { it.supportsLatest }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isValidSource(source: Source?): Boolean {
|
|
||||||
return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -18,9 +18,8 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||||
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
||||||
|
@ -84,8 +83,7 @@ class MainActivity : BaseActivity() {
|
||||||
R.id.nav_drawer_library -> setRoot(LibraryController(), id)
|
R.id.nav_drawer_library -> setRoot(LibraryController(), id)
|
||||||
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
|
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
|
||||||
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
|
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
|
||||||
R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
|
R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id)
|
||||||
R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id)
|
|
||||||
R.id.nav_drawer_downloads -> {
|
R.id.nav_drawer_downloads -> {
|
||||||
router.pushController(RouterTransaction.with(DownloadController())
|
router.pushController(RouterTransaction.with(DownloadController())
|
||||||
.pushChangeHandler(FadeChangeHandler())
|
.pushChangeHandler(FadeChangeHandler())
|
||||||
|
|
|
@ -30,12 +30,6 @@ class SettingsMainController : SettingsController() {
|
||||||
titleRes = R.string.pref_category_downloads
|
titleRes = R.string.pref_category_downloads
|
||||||
onClick { navigateTo(SettingsDownloadController()) }
|
onClick { navigateTo(SettingsDownloadController()) }
|
||||||
}
|
}
|
||||||
preference {
|
|
||||||
iconRes = R.drawable.ic_language_black_24dp
|
|
||||||
iconTint = tintColor
|
|
||||||
titleRes = R.string.pref_category_sources
|
|
||||||
onClick { navigateTo(SettingsSourcesController()) }
|
|
||||||
}
|
|
||||||
preference {
|
preference {
|
||||||
iconRes = R.drawable.ic_sync_black_24dp
|
iconRes = R.drawable.ic_sync_black_24dp
|
||||||
iconTint = tintColor
|
iconTint = tintColor
|
||||||
|
|
|
@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.setting
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.support.v7.preference.PreferenceGroup
|
import android.support.v7.preference.PreferenceGroup
|
||||||
import android.support.v7.preference.PreferenceScreen
|
import android.support.v7.preference.PreferenceScreen
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
|
|
@ -105,7 +105,7 @@ val Context.powerManager: PowerManager
|
||||||
*
|
*
|
||||||
* @param intent intent that contains broadcast information
|
* @param intent intent that contains broadcast information
|
||||||
*/
|
*/
|
||||||
fun Context.sendLocalBroadcast(intent:Intent){
|
fun Context.sendLocalBroadcast(intent: Intent) {
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,12 @@ package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Point
|
import android.graphics.Point
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns coordinates of view.
|
* Returns coordinates of view.
|
||||||
|
@ -43,3 +46,21 @@ inline fun View.invisible() {
|
||||||
inline fun View.gone() {
|
inline fun View.gone() {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a TextDrawable determined by input
|
||||||
|
*
|
||||||
|
* @param text text of [TextDrawable]
|
||||||
|
* @param random random color
|
||||||
|
*/
|
||||||
|
fun View.getRound(text: String, random : Boolean = true): TextDrawable {
|
||||||
|
val size = Math.min(this.width, this.height)
|
||||||
|
return TextDrawable.builder()
|
||||||
|
.beginConfig()
|
||||||
|
.width(size)
|
||||||
|
.height(size)
|
||||||
|
.textColor(Color.WHITE)
|
||||||
|
.useFont(Typeface.DEFAULT)
|
||||||
|
.endConfig()
|
||||||
|
.buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text))
|
||||||
|
}
|
|
@ -18,6 +18,4 @@
|
||||||
</item>
|
</item>
|
||||||
</selector>
|
</selector>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
|
||||||
</ripple>
|
</ripple>
|
|
@ -18,6 +18,4 @@
|
||||||
</item>
|
</item>
|
||||||
</selector>
|
</selector>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
|
||||||
</ripple>
|
</ripple>
|
|
@ -18,6 +18,4 @@
|
||||||
</item>
|
</item>
|
||||||
</selector>
|
</selector>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
|
||||||
</ripple>
|
</ripple>
|
|
@ -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,13 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--<selector android:exitFadeDuration="@android:integer/config_longAnimTime"-->
|
|
||||||
<!--xmlns:android="http://schemas.android.com/apk/res/android">-->
|
|
||||||
|
|
||||||
<!--<item android:state_focused="true" android:drawable="?attr/colorAccent"/>-->
|
|
||||||
<!--<item android:state_pressed="true" android:drawable="?attr/colorAccent"/>-->
|
|
||||||
<!--<item android:state_activated="true" android:drawable="?attr/colorAccent"/>-->
|
|
||||||
<!--<item android:drawable="?android:attr/colorBackground"/>-->
|
|
||||||
<!--</selector>-->
|
|
||||||
|
|
||||||
<selector android:exitFadeDuration="@android:integer/config_longAnimTime"
|
<selector android:exitFadeDuration="@android:integer/config_longAnimTime"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--<selector android:exitFadeDuration="@android:integer/config_longAnimTime"-->
|
|
||||||
<!--xmlns:android="http://schemas.android.com/apk/res/android">-->
|
|
||||||
|
|
||||||
<!--<item android:state_focused="true" android:drawable="?attr/colorAccent"/>-->
|
|
||||||
<!--<item android:state_pressed="true" android:drawable="?attr/colorAccent"/>-->
|
|
||||||
<!--<item android:state_activated="true" android:drawable="?attr/colorAccent"/>-->
|
|
||||||
<!--<item android:drawable="?android:attr/colorBackground"/>-->
|
|
||||||
<!--</selector>-->
|
|
||||||
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:exitFadeDuration="@android:integer/config_longAnimTime">
|
android:exitFadeDuration="@android:integer/config_longAnimTime">
|
||||||
|
|
||||||
|
|
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>
|
|
@ -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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectable_library_drawable">
|
android:background="?selectable_library_drawable">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -14,8 +14,7 @@
|
||||||
android:paddingEnd="0dp"
|
android:paddingEnd="0dp"
|
||||||
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
||||||
android:paddingRight="0dp"
|
android:paddingRight="0dp"
|
||||||
android:paddingStart="@dimen/material_component_lists_icon_left_padding"
|
android:paddingStart="@dimen/material_component_lists_icon_left_padding"/>
|
||||||
tools:src="@drawable/icon"/>
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
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:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/catalogue_grid"
|
android:id="@+id/catalogue_grid"
|
||||||
style="@style/Theme.Widget.GridView"
|
style="@style/Theme.Widget.GridView.Catalogue"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:columnWidth="140dp"
|
android:columnWidth="140dp"
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
||||||
android:paddingStart="@dimen/material_component_lists_icon_left_padding"
|
android:paddingStart="@dimen/material_component_lists_icon_left_padding"
|
||||||
android:paddingRight="0dp"
|
android:paddingRight="0dp"
|
||||||
android:paddingEnd="0dp"/>
|
android:paddingEnd="0dp"
|
||||||
|
tools:src="@mipmap/ic_launcher_round"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/title"
|
android:id="@+id/title"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/library_grid"
|
android:id="@+id/library_grid"
|
||||||
style="@style/Theme.Widget.GridView"
|
style="@style/Theme.Widget.GridView.Catalogue"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:columnWidth="140dp"
|
android:columnWidth="140dp"
|
||||||
|
|
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_read">Last read</string>
|
||||||
<string name="action_sort_last_updated">Last updated</string>
|
<string name="action_sort_last_updated">Last updated</string>
|
||||||
<string name="action_search">Search</string>
|
<string name="action_search">Search</string>
|
||||||
|
<string name="action_global_search">Global search</string>
|
||||||
<string name="action_select_all">Select all</string>
|
<string name="action_select_all">Select all</string>
|
||||||
<string name="action_mark_as_read">Mark as read</string>
|
<string name="action_mark_as_read">Mark as read</string>
|
||||||
<string name="action_mark_as_unread">Mark as unread</string>
|
<string name="action_mark_as_unread">Mark as unread</string>
|
||||||
|
@ -85,6 +86,8 @@
|
||||||
<string name="action_open_log">Open log</string>
|
<string name="action_open_log">Open log</string>
|
||||||
<string name="action_create">Create</string>
|
<string name="action_create">Create</string>
|
||||||
<string name="action_restore">Restore</string>
|
<string name="action_restore">Restore</string>
|
||||||
|
<string name="action_open">Open</string>
|
||||||
|
<string name="action_login">Login</string>
|
||||||
|
|
||||||
<!-- Operations -->
|
<!-- Operations -->
|
||||||
<string name="deleting">Deleting…</string>
|
<string name="deleting">Deleting…</string>
|
||||||
|
@ -276,8 +279,13 @@
|
||||||
<string name="no_valid_sources">Please enable at least one valid source</string>
|
<string name="no_valid_sources">Please enable at least one valid source</string>
|
||||||
<string name="no_more_results">No more results</string>
|
<string name="no_more_results">No more results</string>
|
||||||
<string name="local_source">Local manga</string>
|
<string name="local_source">Local manga</string>
|
||||||
|
<string name="other_source">Other</string>
|
||||||
<string name="invalid_combination">Default can\'t be selected with other categories</string>
|
<string name="invalid_combination">Default can\'t be selected with other categories</string>
|
||||||
<string name="added_to_library">The manga has been added to your library</string>
|
<string name="added_to_library">The manga has been added to your library</string>
|
||||||
|
<string name="action_global_search_hint">Global search…</string>
|
||||||
|
<string name="no_results">No results found!</string>
|
||||||
|
<string name="latest">Latest</string>
|
||||||
|
<string name="browse">Browse</string>
|
||||||
|
|
||||||
<!-- Manga activity -->
|
<!-- Manga activity -->
|
||||||
<string name="manga_not_in_db">This manga was removed from the database!</string>
|
<string name="manga_not_in_db">This manga was removed from the database!</string>
|
||||||
|
@ -430,5 +438,4 @@
|
||||||
<string name="download_notifier_text_only_wifi">No wifi connection available</string>
|
<string name="download_notifier_text_only_wifi">No wifi connection available</string>
|
||||||
<string name="download_notifier_no_network">No network connection available</string>
|
<string name="download_notifier_no_network">No network connection available</string>
|
||||||
<string name="download_notifier_download_paused">Download paused</string>
|
<string name="download_notifier_download_paused">Download paused</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<!--========-->
|
<!--========-->
|
||||||
<!--Toolbars-->
|
<!--Toolbars-->
|
||||||
<!--========-->
|
<!--========-->
|
||||||
<style name="Theme.ActionBar" parent="@style/ThemeOverlay.AppCompat.ActionBar"/>
|
<style name="Theme.ActionBar" parent="@style/ThemeOverlay.AppCompat.ActionBar" />
|
||||||
|
|
||||||
<style name="Theme.ActionBar.Light" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
<style name="Theme.ActionBar.Light" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||||
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
|
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
|
||||||
|
@ -13,12 +13,12 @@
|
||||||
<!--====-->
|
<!--====-->
|
||||||
<!--Tabs-->
|
<!--Tabs-->
|
||||||
<!--====-->
|
<!--====-->
|
||||||
<style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
|
<style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
<!--===========-->
|
<!--===========-->
|
||||||
<!--AlertDialog-->
|
<!--AlertDialog-->
|
||||||
<!--===========-->
|
<!--===========-->
|
||||||
<style name="Theme.AlertDialog"/>
|
<style name="Theme.AlertDialog" />
|
||||||
|
|
||||||
<style name="Theme.AlertDialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert">
|
<style name="Theme.AlertDialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert">
|
||||||
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
|
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
<!--==============-->
|
<!--==============-->
|
||||||
<!--NavigationView-->
|
<!--NavigationView-->
|
||||||
<!--==============-->
|
<!--==============-->
|
||||||
<style name="Theme.Widget.NavigationView"/>
|
<style name="Theme.Widget.NavigationView" />
|
||||||
|
|
||||||
<style name="Theme.Widget.NavigationView.Dark">
|
<style name="Theme.Widget.NavigationView.Dark">
|
||||||
<item name="colorControlHighlight">@color/md_grey_900</item>
|
<item name="colorControlHighlight">@color/md_grey_900</item>
|
||||||
|
@ -85,6 +85,10 @@
|
||||||
<item name="android:textSize">16sp</item>
|
<item name="android:textSize">16sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.Regular.SubHeading.Upper">
|
||||||
|
<item name="android:textAllCaps">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="TextAppearance.Regular.SubHeading.Secondary">
|
<style name="TextAppearance.Regular.SubHeading.Secondary">
|
||||||
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -105,6 +109,10 @@
|
||||||
<item name="android:textSize">20sp</item>
|
<item name="android:textSize">20sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.Medium.Title.Upper">
|
||||||
|
<item name="android:textAllCaps">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="TextAppearance.Medium.Title.Secondary">
|
<style name="TextAppearance.Medium.Title.Secondary">
|
||||||
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -130,7 +138,7 @@
|
||||||
<!--=======-->
|
<!--=======-->
|
||||||
<!--Widgets-->
|
<!--Widgets-->
|
||||||
<!--=======-->
|
<!--=======-->
|
||||||
<style name="Theme.Widget"/>
|
<style name="Theme.Widget" />
|
||||||
|
|
||||||
<style name="Theme.Widget.FAB">
|
<style name="Theme.Widget.FAB">
|
||||||
<item name="android:layout_height">@dimen/fab_size</item>
|
<item name="android:layout_height">@dimen/fab_size</item>
|
||||||
|
@ -147,10 +155,16 @@
|
||||||
<item name="android:layout_width">match_parent</item>
|
<item name="android:layout_width">match_parent</item>
|
||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="android:layout_height">wrap_content</item>
|
||||||
<item name="android:padding">@dimen/material_component_cards_top_and_bottom_padding</item>
|
<item name="android:padding">@dimen/material_component_cards_top_and_bottom_padding</item>
|
||||||
<item name="android:layout_marginTop">@dimen/material_component_cards_space_between_cards</item>
|
<item name="android:layout_marginTop">@dimen/material_component_cards_space_between_cards
|
||||||
<item name="android:layout_marginBottom">@dimen/material_component_cards_space_between_cards</item>
|
</item>
|
||||||
<item name="android:layout_marginStart">@dimen/material_component_cards_space_between_cards</item>
|
<item name="android:layout_marginBottom">
|
||||||
<item name="android:layout_marginEnd">@dimen/material_component_cards_space_between_cards</item>
|
@dimen/material_component_cards_space_between_cards
|
||||||
|
</item>
|
||||||
|
<item name="android:layout_marginStart">
|
||||||
|
@dimen/material_component_cards_space_between_cards
|
||||||
|
</item>
|
||||||
|
<item name="android:layout_marginEnd">@dimen/material_component_cards_space_between_cards
|
||||||
|
</item>
|
||||||
<item name="cardBackgroundColor">?attr/background_card</item>
|
<item name="cardBackgroundColor">?attr/background_card</item>
|
||||||
<item name="cardElevation">2dp</item>
|
<item name="cardElevation">2dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -161,21 +175,24 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Widget.GridView">
|
<style name="Theme.Widget.GridView">
|
||||||
|
<item name="android:smoothScrollbar">true</item>
|
||||||
|
<item name="android:numColumns">auto_fit</item>
|
||||||
|
<item name="android:stretchMode">columnWidth</item>
|
||||||
|
<item name="android:scrollbarStyle">outsideOverlay</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.Widget.GridView.Catalogue">
|
||||||
<item name="android:padding">5dp</item>
|
<item name="android:padding">5dp</item>
|
||||||
<item name="android:clipToPadding">false</item>
|
|
||||||
<item name="android:gravity">top|left</item>
|
<item name="android:gravity">top|left</item>
|
||||||
<item name="android:smoothScrollbar">true</item>
|
<item name="android:smoothScrollbar">true</item>
|
||||||
<item name="android:cacheColorHint">?android:attr/textColorHint</item>
|
<item name="android:cacheColorHint">?android:attr/textColorHint</item>
|
||||||
<item name="android:fastScrollEnabled">true</item>
|
<item name="android:fastScrollEnabled">true</item>
|
||||||
<item name="android:horizontalSpacing">0dp</item>
|
<item name="android:horizontalSpacing">0dp</item>
|
||||||
<item name="android:verticalSpacing">0dp</item>
|
<item name="android:verticalSpacing">0dp</item>
|
||||||
<item name="android:numColumns">auto_fit</item>
|
|
||||||
<item name="android:stretchMode">columnWidth</item>
|
|
||||||
<item name="android:scrollbarStyle">outsideOverlay</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="Theme.Widget.CheckBox"/>
|
<style name="Theme.Widget.CheckBox" />
|
||||||
|
|
||||||
<style name="Theme.Widget.CheckBox.Light" parent="TextAppearance.Regular.Body1.Light">
|
<style name="Theme.Widget.CheckBox.Light" parent="TextAppearance.Regular.Body1.Light">
|
||||||
<item name="buttonTint">@color/md_white_1000</item>
|
<item name="buttonTint">@color/md_white_1000</item>
|
||||||
|
@ -212,8 +229,7 @@
|
||||||
<item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
<item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert">
|
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"></style>
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="reader_settings_popup_animation">
|
<style name="reader_settings_popup_animation">
|
||||||
<item name="android:windowEnterAnimation">@anim/enter_from_right</item>
|
<item name="android:windowEnterAnimation">@anim/enter_from_right</item>
|
||||||
|
@ -226,5 +242,4 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Reference in a new issue