Add custom browese view, disabled by default and can be enabled in the settings
Signed-off-by: Jobobby04 <jobobby04@users.noreply.github.com>
This commit is contained in:
parent
9e0e2db25d
commit
2e4def13e3
@ -284,4 +284,6 @@ object PreferenceKeys {
|
||||
const val continuousVerticalTappingByPage = "continuous_vertical_tapping_by_page"
|
||||
|
||||
const val groupLibraryUpdateType = "group_library_update_type"
|
||||
|
||||
const val useNewSourceNavigation = "use_new_source_navigation"
|
||||
}
|
||||
|
@ -389,4 +389,6 @@ class PreferencesHelper(val context: Context) {
|
||||
fun continuousVerticalTappingByPage() = flowPrefs.getBoolean(Keys.continuousVerticalTappingByPage, false)
|
||||
|
||||
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
|
||||
|
||||
fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, false)
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.index.IndexController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
@ -152,7 +153,9 @@ class SourceController(bundle: Bundle? = null) :
|
||||
when (mode) {
|
||||
Mode.CATALOGUE -> {
|
||||
// Open the catalogue view.
|
||||
openSource(source, BrowseSourceController(source))
|
||||
// SY -->
|
||||
if (source.supportsLatest && preferences.useNewSourceNavigation().get()) openIndexSource(source) else openSource(source, BrowseSourceController(source))
|
||||
// SY <--
|
||||
}
|
||||
Mode.SMART_SEARCH -> router.pushController(
|
||||
SmartSearchController(
|
||||
@ -319,6 +322,16 @@ class SourceController(bundle: Bundle? = null) :
|
||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Opens a catalogue with the index controller.
|
||||
*/
|
||||
private fun openIndexSource(source: CatalogueSource) {
|
||||
preferences.lastUsedSource().set(source.id)
|
||||
parentController!!.router.pushController(IndexController(source).withFadeTransaction())
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
|
@ -17,7 +17,7 @@ import kotlinx.android.synthetic.main.source_main_controller_card_item.pin
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
||||
|
||||
class SourceHolder(private val view: View, override val adapter: SourceAdapter /* SY --> */, private val showButtons: Boolean /* SY <-- */) :
|
||||
class SourceHolder(private val view: View, override val adapter: SourceAdapter /* SY --> */, private val showLatest: Boolean, private val showPins: Boolean /* SY <-- */) :
|
||||
BaseFlexibleViewHolder(view, adapter),
|
||||
SourceListItem,
|
||||
SlicedHolder {
|
||||
@ -39,7 +39,7 @@ class SourceHolder(private val view: View, override val adapter: SourceAdapter /
|
||||
}
|
||||
|
||||
// SY -->
|
||||
if (!showButtons) {
|
||||
if (!showLatest) {
|
||||
source_latest.isVisible = false
|
||||
}
|
||||
// SY <--
|
||||
@ -61,9 +61,9 @@ class SourceHolder(private val view: View, override val adapter: SourceAdapter /
|
||||
}
|
||||
}
|
||||
|
||||
source_latest.isVisible = source.supportsLatest/* SY --> */ && showButtons /* SY <-- */
|
||||
source_latest.isVisible = source.supportsLatest/* SY --> */ && showLatest /* SY <-- */
|
||||
|
||||
pin.isVisible = showButtons
|
||||
pin.isVisible = showPins
|
||||
if (item.isPinned) {
|
||||
pin.setVectorCompat(R.drawable.ic_push_pin_filled_24dp, view.context.getResourceColor(R.attr.colorAccent))
|
||||
} else {
|
||||
|
@ -19,7 +19,8 @@ data class SourceItem(
|
||||
val header: LangItem? = null,
|
||||
val isPinned: Boolean = false,
|
||||
// SY -->
|
||||
val showButtons: Boolean
|
||||
val showLatest: Boolean,
|
||||
val showPins: Boolean
|
||||
// SY <--
|
||||
) :
|
||||
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
||||
@ -35,7 +36,7 @@ data class SourceItem(
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
||||
return SourceHolder(view, adapter as SourceAdapter /* SY --> */, showButtons /* SY <-- */)
|
||||
return SourceHolder(view, adapter as SourceAdapter /* SY --> */, showLatest, showPins /* SY <-- */)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,9 +91,13 @@ class SourcePresenter(
|
||||
var sourceItems = byLang.flatMap {
|
||||
val langItem = LangItem(it.key)
|
||||
it.value.map { source ->
|
||||
// SY -->
|
||||
val showPins = controllerMode == SourceController.Mode.CATALOGUE
|
||||
val showLatest = showPins && !preferences.useNewSourceNavigation().get()
|
||||
// SY <--
|
||||
val isPinned = source.id.toString() in pinnedSourceIds
|
||||
if (isPinned) {
|
||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), isPinned, controllerMode == SourceController.Mode.CATALOGUE))
|
||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), isPinned /* SY --> */, showLatest, showPins /* SY <-- */))
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@ -108,7 +112,8 @@ class SourcePresenter(
|
||||
source,
|
||||
LangItem("custom|" + SourceAndCategory.second),
|
||||
isPinned,
|
||||
controllerMode == SourceController.Mode.CATALOGUE
|
||||
showLatest,
|
||||
showPins
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -117,7 +122,7 @@ class SourcePresenter(
|
||||
}
|
||||
// SY <--
|
||||
|
||||
SourceItem(source, langItem, isPinned, controllerMode == SourceController.Mode.CATALOGUE)
|
||||
SourceItem(source, langItem, isPinned /* SY --> */, showLatest, showPins /* SY <-- */)
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +156,11 @@ class SourcePresenter(
|
||||
private fun updateLastUsedSource(sourceId: Long) {
|
||||
val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let {
|
||||
val isPinned = it.id.toString() in preferences.pinnedSources().get()
|
||||
SourceItem(it, null, isPinned, controllerMode == SourceController.Mode.CATALOGUE)
|
||||
// SY -->
|
||||
val showPins = controllerMode == SourceController.Mode.CATALOGUE
|
||||
val showLatest = showPins && !preferences.useNewSourceNavigation().get()
|
||||
// SY <--
|
||||
SourceItem(it, null, isPinned /* SY --> */, showLatest, showPins /* SY <-- */)
|
||||
}
|
||||
source?.let { view?.setLastUsedSource(it) }
|
||||
}
|
||||
|
@ -82,7 +82,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
source: CatalogueSource,
|
||||
searchQuery: String? = null,
|
||||
// SY -->
|
||||
smartSearchConfig: SourceController.SmartSearchConfig? = null
|
||||
smartSearchConfig: SourceController.SmartSearchConfig? = null,
|
||||
filterList: String? = null
|
||||
// SY <--
|
||||
) : this(
|
||||
Bundle().apply {
|
||||
@ -96,6 +97,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
if (smartSearchConfig != null) {
|
||||
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
|
||||
}
|
||||
|
||||
if (filterList != null) {
|
||||
putString(FILTERS_CONFIG_KEY, filterList)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
)
|
||||
@ -160,7 +165,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
return BrowseSourcePresenter(
|
||||
args.getLong(SOURCE_ID_KEY),
|
||||
args.getString(SEARCH_QUERY_KEY),
|
||||
recommendsMangaId = if (mode == Mode.RECOMMENDS) recommendsConfig?.mangaId else null
|
||||
recommendsMangaId = if (mode == Mode.RECOMMENDS) recommendsConfig?.mangaId else null,
|
||||
filters = args.getString(FILTERS_CONFIG_KEY)
|
||||
)
|
||||
// SY <--
|
||||
}
|
||||
@ -851,6 +857,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
|
||||
// SY -->
|
||||
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
||||
const val FILTERS_CONFIG_KEY = "filters"
|
||||
const val RECOMMENDS_CONFIG = "RECOMMENDS_CONFIG"
|
||||
// SY <--
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ open class BrowseSourcePresenter(
|
||||
private val searchQuery: String? = null,
|
||||
// SY -->
|
||||
private val recommendsMangaId: Long? = null,
|
||||
private val filters: String? = null,
|
||||
// SY <--
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
@ -115,6 +116,8 @@ open class BrowseSourcePresenter(
|
||||
|
||||
// SY -->
|
||||
private var manga: Manga? = null
|
||||
|
||||
private val filterSerializer = FilterSerializer()
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
@ -129,6 +132,14 @@ open class BrowseSourcePresenter(
|
||||
|
||||
sourceFilters = source.getFilterList()
|
||||
|
||||
// SY -->
|
||||
if (filters != null) {
|
||||
val filters = JsonParser.parseString(filters).obj
|
||||
filterSerializer.deserialize(sourceFilters, filters["filters"].array)
|
||||
}
|
||||
val allDefault = sourceFilters == source.getFilterList()
|
||||
// SY <--
|
||||
|
||||
if (savedState != null) {
|
||||
query = savedState.getString(::query.name, "")
|
||||
}
|
||||
@ -137,7 +148,7 @@ open class BrowseSourcePresenter(
|
||||
manga = db.getManga(recommendsMangaId).executeAsBlocking()
|
||||
}
|
||||
|
||||
restartPager()
|
||||
restartPager(/* SY -->*/ filters = if (allDefault) this.appliedFilters else sourceFilters /* SY <--*/)
|
||||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
@ -323,48 +334,54 @@ open class BrowseSourcePresenter(
|
||||
return SourcePager(source, query, filters)
|
||||
}
|
||||
|
||||
private fun FilterList.toItems(): List<IFlexible<*>> {
|
||||
return mapNotNull { filter ->
|
||||
when (filter) {
|
||||
is Filter.Header -> HeaderItem(filter)
|
||||
// --> EXH
|
||||
is Filter.HelpDialog -> HelpDialogItem(filter)
|
||||
is Filter.AutoComplete -> AutoComplete(filter)
|
||||
// <-- EXH
|
||||
is Filter.Separator -> SeparatorItem(filter)
|
||||
is Filter.CheckBox -> CheckboxItem(filter)
|
||||
is Filter.TriState -> TriStateItem(filter)
|
||||
is Filter.Text -> TextItem(filter)
|
||||
is Filter.Select<*> -> SelectItem(filter)
|
||||
is Filter.Group<*> -> {
|
||||
val group = GroupItem(filter)
|
||||
val subItems = filter.state.mapNotNull {
|
||||
when (it) {
|
||||
is Filter.CheckBox -> CheckboxSectionItem(it)
|
||||
is Filter.TriState -> TriStateSectionItem(it)
|
||||
is Filter.Text -> TextSectionItem(it)
|
||||
is Filter.Select<*> -> SelectSectionItem(it)
|
||||
// SY -->
|
||||
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
|
||||
// SY <--
|
||||
else -> null
|
||||
} as? ISectionable<*, *>
|
||||
// SY -->
|
||||
companion object {
|
||||
// SY <--
|
||||
fun FilterList.toItems(): List<IFlexible<*>> {
|
||||
return mapNotNull { filter ->
|
||||
when (filter) {
|
||||
is Filter.Header -> HeaderItem(filter)
|
||||
// --> EXH
|
||||
is Filter.HelpDialog -> HelpDialogItem(filter)
|
||||
is Filter.AutoComplete -> AutoComplete(filter)
|
||||
// <-- EXH
|
||||
is Filter.Separator -> SeparatorItem(filter)
|
||||
is Filter.CheckBox -> CheckboxItem(filter)
|
||||
is Filter.TriState -> TriStateItem(filter)
|
||||
is Filter.Text -> TextItem(filter)
|
||||
is Filter.Select<*> -> SelectItem(filter)
|
||||
is Filter.Group<*> -> {
|
||||
val group = GroupItem(filter)
|
||||
val subItems = filter.state.mapNotNull {
|
||||
when (it) {
|
||||
is Filter.CheckBox -> CheckboxSectionItem(it)
|
||||
is Filter.TriState -> TriStateSectionItem(it)
|
||||
is Filter.Text -> TextSectionItem(it)
|
||||
is Filter.Select<*> -> SelectSectionItem(it)
|
||||
// SY -->
|
||||
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
|
||||
// SY <--
|
||||
else -> null
|
||||
} as? ISectionable<*, *>
|
||||
}
|
||||
subItems.forEach { it.header = group }
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
subItems.forEach { it.header = group }
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
val group = SortGroup(filter)
|
||||
val subItems = filter.values.map {
|
||||
SortItem(it, group)
|
||||
is Filter.Sort -> {
|
||||
val group = SortGroup(filter)
|
||||
val subItems = filter.values.map {
|
||||
SortItem(it, group)
|
||||
}
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY -->
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Get user categories.
|
||||
@ -422,7 +439,6 @@ open class BrowseSourcePresenter(
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
private val filterSerializer = FilterSerializer()
|
||||
fun saveSearches(searches: List<EXHSavedSearch>) {
|
||||
val otherSerialized = prefs.eh_savedSearches().get().filter {
|
||||
!it.startsWith("${source.id}:")
|
||||
|
@ -0,0 +1,155 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.IndexAdapterBinding
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
||||
/**
|
||||
* Adapter that holds the search cards.
|
||||
*
|
||||
* @param controller instance of [IndexController].
|
||||
*/
|
||||
class IndexAdapter(val controller: IndexController) :
|
||||
RecyclerView.Adapter<IndexAdapter.ViewHolder>() {
|
||||
|
||||
val clickListener: ClickListener = controller
|
||||
|
||||
private lateinit var binding: IndexAdapterBinding
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
var holder: IndexAdapter.ViewHolder? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IndexAdapter.ViewHolder {
|
||||
binding = IndexAdapterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: IndexAdapter.ViewHolder, position: Int) {
|
||||
this.holder = holder
|
||||
holder.bindBrowse(null)
|
||||
holder.bindLatest(null)
|
||||
}
|
||||
|
||||
// stores and recycles views as they are scrolled off screen
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val browseAdapter = IndexCardAdapter(controller)
|
||||
private var browseLastBoundResults: List<IndexCardItem>? = null
|
||||
|
||||
private val latestAdapter = IndexCardAdapter(controller)
|
||||
private var latestLastBoundResults: List<IndexCardItem>? = null
|
||||
|
||||
init {
|
||||
binding.browseBarWrapper.clicks()
|
||||
.onEach {
|
||||
clickListener.onBrowseClick()
|
||||
}
|
||||
.launchIn(scope)
|
||||
binding.latestBarWrapper.clicks()
|
||||
.onEach {
|
||||
clickListener.onLatestClick()
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
binding.browseRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.browseRecycler.adapter = browseAdapter
|
||||
|
||||
binding.latestRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.latestRecycler.adapter = latestAdapter
|
||||
}
|
||||
|
||||
fun bindBrowse(browseResults: List<IndexCardItem>?) {
|
||||
when {
|
||||
browseResults == null -> {
|
||||
binding.browseProgress.isVisible = true
|
||||
showBrowseResultsHolder()
|
||||
}
|
||||
browseResults.isEmpty() -> {
|
||||
binding.browseProgress.isVisible = false
|
||||
showBrowseNoResults()
|
||||
}
|
||||
else -> {
|
||||
binding.browseProgress.isVisible = false
|
||||
showBrowseResultsHolder()
|
||||
}
|
||||
}
|
||||
if (browseResults !== browseLastBoundResults) {
|
||||
browseAdapter.updateDataSet(browseResults)
|
||||
browseLastBoundResults = browseResults
|
||||
}
|
||||
}
|
||||
|
||||
fun bindLatest(latestResults: List<IndexCardItem>?) {
|
||||
when {
|
||||
latestResults == null -> {
|
||||
binding.latestProgress.isVisible = true
|
||||
showLatestResultsHolder()
|
||||
}
|
||||
latestResults.isEmpty() -> {
|
||||
binding.latestProgress.isVisible = false
|
||||
showLatestNoResults()
|
||||
}
|
||||
else -> {
|
||||
binding.latestProgress.isVisible = false
|
||||
showLatestResultsHolder()
|
||||
}
|
||||
}
|
||||
if (latestResults !== latestLastBoundResults) {
|
||||
latestAdapter.updateDataSet(latestResults)
|
||||
latestLastBoundResults = latestResults
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBrowseResultsHolder() {
|
||||
binding.browseNoResultsFound.isVisible = false
|
||||
binding.browseCard.isVisible = true
|
||||
}
|
||||
|
||||
private fun showBrowseNoResults() {
|
||||
binding.browseNoResultsFound.isVisible = true
|
||||
binding.browseCard.isVisible = false
|
||||
}
|
||||
|
||||
private fun showLatestResultsHolder() {
|
||||
binding.latestNoResultsFound.isVisible = false
|
||||
binding.latestCard.isVisible = true
|
||||
}
|
||||
|
||||
private fun showLatestNoResults() {
|
||||
binding.latestNoResultsFound.isVisible = true
|
||||
binding.latestCard.isVisible = false
|
||||
}
|
||||
|
||||
fun setLatestImage(manga: Manga) {
|
||||
latestAdapter.allBoundViewHolders.forEach {
|
||||
if (it !is IndexCardHolder) return@forEach
|
||||
if (latestAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
|
||||
it.setImage(manga)
|
||||
}
|
||||
}
|
||||
fun setBrowseImage(manga: Manga) {
|
||||
browseAdapter.allBoundViewHolders.forEach {
|
||||
if (it !is IndexCardHolder) return@forEach
|
||||
if (browseAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
|
||||
it.setImage(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ClickListener {
|
||||
fun onBrowseClick(search: String? = null, filters: String? = null)
|
||||
fun onLatestClick()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
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 [IndexController].
|
||||
*/
|
||||
class IndexCardAdapter(controller: IndexController) :
|
||||
FlexibleAdapter<IndexCardItem>(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 [IndexController]
|
||||
*/
|
||||
interface OnMangaClickListener {
|
||||
fun onMangaClick(manga: Manga)
|
||||
fun onMangaLongClick(manga: Manga)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
import kotlinx.android.synthetic.main.global_search_controller_comfortable_card_item.itemImage
|
||||
import kotlinx.android.synthetic.main.global_search_controller_comfortable_card_item.progress
|
||||
import kotlinx.android.synthetic.main.global_search_controller_comfortable_card_item.tvTitle
|
||||
|
||||
class IndexCardHolder(view: View, adapter: IndexCardAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
init {
|
||||
// Call onMangaClickListener when item is pressed.
|
||||
itemView.setOnClickListener {
|
||||
val item = adapter.getItem(bindingAdapterPosition)
|
||||
if (item != null) {
|
||||
adapter.mangaClickListener.onMangaClick(item.manga)
|
||||
}
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
val item = adapter.getItem(bindingAdapterPosition)
|
||||
if (item != null) {
|
||||
adapter.mangaClickListener.onMangaLongClick(item.manga)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(manga: Manga) {
|
||||
tvTitle.text = manga.title
|
||||
// Set alpha of thumbnail.
|
||||
itemImage.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||
|
||||
setImage(manga)
|
||||
}
|
||||
|
||||
fun setImage(manga: Manga) {
|
||||
GlideApp.with(itemView.context).clear(itemImage)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(itemView.context)
|
||||
.load(manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(itemImage, progress))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.browse.latest.LatestCardItem
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class IndexCardItem(val manga: Manga) : AbstractFlexibleItem<IndexCardHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return when (Injekt.get<PreferencesHelper>().sourceDisplayMode().get()) {
|
||||
PreferenceValues.DisplayMode.COMPACT_GRID -> R.layout.global_search_controller_compact_card_item
|
||||
else -> R.layout.global_search_controller_comfortable_card_item
|
||||
}
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): IndexCardHolder {
|
||||
return IndexCardHolder(view, adapter as IndexCardAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: IndexCardHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is LatestCardItem) {
|
||||
return manga.id == other.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return manga.id?.toInt() ?: 0
|
||||
}
|
||||
}
|
@ -0,0 +1,302 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.github.salomonbrys.kotson.jsonObject
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.LatestControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
|
||||
/**
|
||||
* 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 [IndexPresenter]
|
||||
* [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
||||
*/
|
||||
open class IndexController :
|
||||
NucleusController<LatestControllerBinding, IndexPresenter>,
|
||||
FabController,
|
||||
IndexCardAdapter.OnMangaClickListener,
|
||||
IndexAdapter.ClickListener {
|
||||
|
||||
constructor(source: CatalogueSource?) : super(
|
||||
Bundle().apply {
|
||||
putLong(SOURCE_EXTRA, source?.id ?: 0)
|
||||
}
|
||||
) {
|
||||
this.source = source
|
||||
}
|
||||
|
||||
constructor(sourceId: Long) : this(
|
||||
Injekt.get<SourceManager>().get(sourceId) as? CatalogueSource
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : this(bundle.getLong(SOURCE_EXTRA))
|
||||
|
||||
var source: CatalogueSource? = null
|
||||
|
||||
/**
|
||||
* Adapter containing search results grouped by lang.
|
||||
*/
|
||||
protected var adapter: IndexAdapter? = null
|
||||
|
||||
private var actionFab: ExtendedFloatingActionButton? = null
|
||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||
|
||||
/**
|
||||
* Sheet containing filter items.
|
||||
*/
|
||||
private var filterSheet: SourceFilterSheet? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the view with [R.layout.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): View {
|
||||
binding = LatestControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return source!!.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [LatestPresenter] used in controller.
|
||||
*
|
||||
* @return instance of [LatestPresenter]
|
||||
*/
|
||||
override fun createPresenter(): IndexPresenter {
|
||||
return IndexPresenter(source!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(MangaController(manga, true).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when manga in global search is long clicked.
|
||||
*
|
||||
* @param manga clicked item containing manga information.
|
||||
*/
|
||||
override fun onMangaLongClick(manga: Manga) {
|
||||
// Delegate to single click by default.
|
||||
onMangaClick(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.global_search, menu)
|
||||
|
||||
// Initialize search menu
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
val query = presenter.query
|
||||
if (!query.isBlank()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
searchView.queryTextEvents()
|
||||
.filter { router.backstack.lastOrNull()?.controller() == this@IndexController }
|
||||
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
||||
.onEach { onBrowseClick(presenter.query.nullIfBlank()) }
|
||||
.launchIn(scope)
|
||||
|
||||
searchView.queryTextEvents()
|
||||
.filter { router.backstack.lastOrNull()?.controller() == this@IndexController }
|
||||
.filterIsInstance<QueryTextEvent.QueryChanged>()
|
||||
.onEach { presenter.query = it.queryText.toString() }
|
||||
.launchIn(scope)
|
||||
|
||||
searchItem.fixExpand(
|
||||
onExpand = { invalidateMenuOnExpand() }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
*
|
||||
* @param view view of controller
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Prepare filter sheet
|
||||
initFilterSheet()
|
||||
|
||||
adapter = IndexAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
presenter.getLatest()
|
||||
}
|
||||
|
||||
private val filterSerializer = FilterSerializer()
|
||||
|
||||
open fun initFilterSheet() {
|
||||
if (presenter.sourceFilters.isEmpty()) {
|
||||
actionFab?.text = activity!!.getString(R.string.saved_searches)
|
||||
}
|
||||
|
||||
filterSheet = SourceFilterSheet(
|
||||
activity!!,
|
||||
// SY -->
|
||||
presenter.loadSearches(),
|
||||
// SY <--
|
||||
onFilterClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
filterSheet?.dismiss()
|
||||
if (!allDefault) {
|
||||
val json = jsonObject("filters" to filterSerializer.serialize(presenter.sourceFilters))
|
||||
XLog.nst().json(json.toString())
|
||||
onBrowseClick(presenter.query.nullIfBlank(), json.toString())
|
||||
}
|
||||
},
|
||||
onResetClicked = {},
|
||||
onSaveClicked = {},
|
||||
onSavedSearchClicked = cb@{ indexToSearch ->
|
||||
val savedSearches = presenter.loadSearches()
|
||||
|
||||
val search = savedSearches.getOrNull(indexToSearch)
|
||||
|
||||
if (search == null) {
|
||||
filterSheet?.context?.let {
|
||||
MaterialDialog(it)
|
||||
.title(R.string.save_search_failed_to_load)
|
||||
.message(R.string.save_search_failed_to_load_message)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
}
|
||||
return@cb
|
||||
}
|
||||
|
||||
presenter.sourceFilters = FilterList(search.filterList)
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
filterSheet?.dismiss()
|
||||
|
||||
if (!allDefault) {
|
||||
val json = jsonObject("filters" to filterSerializer.serialize(presenter.sourceFilters))
|
||||
onBrowseClick(presenter.query.nullIfBlank(), json.toString())
|
||||
}
|
||||
},
|
||||
onSavedSearchDeleteClicked = { _, _ -> }
|
||||
)
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
|
||||
filterSheet?.setOnShowListener { actionFab?.isVisible = false }
|
||||
filterSheet?.setOnDismissListener { actionFab?.isVisible = true }
|
||||
|
||||
actionFab?.setOnClickListener { filterSheet?.show() }
|
||||
|
||||
actionFab?.isVisible = true
|
||||
}
|
||||
|
||||
override fun configureFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFab = fab
|
||||
|
||||
// Controlled by initFilterSheet()
|
||||
fab.isVisible = false
|
||||
|
||||
fab.setText(R.string.action_filter)
|
||||
fab.setIconResource(R.drawable.ic_filter_list_24dp)
|
||||
}
|
||||
|
||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
|
||||
actionFab = null
|
||||
}
|
||||
|
||||
fun setLatestManga(results: List<IndexCardItem>?) {
|
||||
adapter?.holder?.bindLatest(results)
|
||||
}
|
||||
|
||||
fun setBrowseManga(results: List<IndexCardItem>?) {
|
||||
adapter?.holder?.bindBrowse(results)
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onBrowseClick(search: String?, filters: String?) {
|
||||
router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction())
|
||||
}
|
||||
|
||||
override fun onLatestClick() {
|
||||
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the initialized manga.
|
||||
*/
|
||||
fun onMangaInitialized(manga: Manga, isLatest: Boolean) {
|
||||
if (isLatest) adapter?.holder?.setLatestImage(manga)
|
||||
else adapter?.holder?.setBrowseImage(manga)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SOURCE_EXTRA = "source"
|
||||
}
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.index
|
||||
|
||||
import android.os.Bundle
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonParser
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems
|
||||
import exh.EXHSavedSearch
|
||||
import java.lang.RuntimeException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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
|
||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
|
||||
/**
|
||||
* Presenter of [IndexController]
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param source the source.
|
||||
* @param db manages the database calls.
|
||||
* @param preferences manages the preference calls.
|
||||
*/
|
||||
open class IndexPresenter(
|
||||
val source: CatalogueSource,
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<IndexController>() {
|
||||
|
||||
/**
|
||||
* Fetches the different sources by user settings.
|
||||
*/
|
||||
private var fetchSourcesSubscription: Subscription? = null
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = ""
|
||||
|
||||
/**
|
||||
* Subject which fetches image of given manga.
|
||||
*/
|
||||
private val fetchImageSubject = PublishSubject.create<List<Pair<Manga, Boolean>>>()
|
||||
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
var sourceFilters = FilterList()
|
||||
set(value) {
|
||||
field = value
|
||||
filterItems = value.toItems()
|
||||
}
|
||||
|
||||
var filterItems: List<IFlexible<*>> = emptyList()
|
||||
|
||||
/**
|
||||
* Subscription for fetching images of manga.
|
||||
*/
|
||||
private var fetchImageSubscription: Subscription? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchImageSubscription?.unsubscribe()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
sourceFilters = source.getFilterList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates get latest per watching source.
|
||||
*/
|
||||
fun getLatest() {
|
||||
// Create image fetch subscription
|
||||
initializeFetchImageSubscription()
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Observable.just(null).subscribeLatestCache({ view, results ->
|
||||
view.setLatestManga(results)
|
||||
})
|
||||
}
|
||||
if (source.supportsLatest) {
|
||||
val results = source.fetchLatestUpdates(1)
|
||||
.toBlocking()
|
||||
.single()
|
||||
.mangas
|
||||
.take(10)
|
||||
.map { networkToLocalManga(it, source.id) }
|
||||
fetchImage(results, true)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
|
||||
view.setLatestManga(results)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Observable.just(null).subscribeLatestCache({ view, results ->
|
||||
view.setBrowseManga(results)
|
||||
})
|
||||
}
|
||||
|
||||
val results = source.fetchPopularManga(1)
|
||||
.toBlocking()
|
||||
.single()
|
||||
.mangas
|
||||
.take(10)
|
||||
.map { networkToLocalManga(it, source.id) }
|
||||
fetchImage(results, false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
|
||||
view.setBrowseManga(results)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a list of manga.
|
||||
*
|
||||
* @param manga the list of manga to initialize.
|
||||
*/
|
||||
private fun fetchImage(manga: List<Manga>, isLatest: Boolean) {
|
||||
fetchImageSubject.onNext(manga.map { it to isLatest })
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||
*/
|
||||
private fun initializeFetchImageSubscription() {
|
||||
fetchImageSubscription?.unsubscribe()
|
||||
fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io())
|
||||
.flatMap { pair ->
|
||||
Observable.from(pair).filter { it.first.thumbnail_url == null && !it.first.initialized }
|
||||
.concatMap { getMangaDetailsObservable(it.first, source, it.second) }
|
||||
}
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ pair ->
|
||||
@Suppress("DEPRECATION")
|
||||
view?.onMangaInitialized(pair.first, pair.second)
|
||||
},
|
||||
{ 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, isLatest: Boolean): Observable<Pair<Manga, Boolean>> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.flatMap { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
Observable.just(manga to isLatest)
|
||||
}
|
||||
.onErrorResumeNext { Observable.just(manga to isLatest) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected open 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
|
||||
}
|
||||
|
||||
private val filterSerializer = FilterSerializer()
|
||||
|
||||
fun loadSearches(): List<EXHSavedSearch> {
|
||||
val loaded = preferences.eh_savedSearches().get()
|
||||
return loaded.map {
|
||||
try {
|
||||
val id = it.substringBefore(':').toLong()
|
||||
if (id != source.id) return@map null
|
||||
val content = JsonParser.parseString(it.substringAfter(':')).obj
|
||||
val originalFilters = source.getFilterList()
|
||||
filterSerializer.deserialize(originalFilters, content["filters"].array)
|
||||
EXHSavedSearch(
|
||||
content["name"].string,
|
||||
content["query"].string,
|
||||
originalFilters
|
||||
)
|
||||
} catch (t: RuntimeException) {
|
||||
// Load failed
|
||||
Timber.e(t, "Failed to load saved search!")
|
||||
t.printStackTrace()
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
}
|
@ -34,6 +34,12 @@ class SettingsBrowseController : SettingsController() {
|
||||
router.pushController(SourceCategoryController().withFadeTransaction())
|
||||
}
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.useNewSourceNavigation
|
||||
titleRes = R.string.pref_source_navigation
|
||||
summaryRes = R.string.pref_source_navigation_summery
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
|
152
app/src/main/res/layout/index_adapter.xml
Normal file
152
app/src/main/res/layout/index_adapter.xml
Normal file
@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/browse_bar_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/browse"
|
||||
style="@style/TextAppearance.Regular.SubHeading"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:text="@string/browse"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/browse_bar_more_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/browse_bar_more_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/label_more"
|
||||
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_arrow_forward_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/browse_no_results_found"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:paddingEnd="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:paddingBottom="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:text="@string/no_results_found"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/browse_card"
|
||||
style="@style/Theme.Widget.CardView.Item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="144dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/browse_progress"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/browse_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
tools:listitem="@layout/global_search_controller_comfortable_card_item" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/latest_bar_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/latest"
|
||||
style="@style/TextAppearance.Regular.SubHeading"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:text="@string/latest"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/latest_bar_more_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/latest_bar_more_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/label_more"
|
||||
android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_arrow_forward_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/latest_no_results_found"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:paddingEnd="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:paddingBottom="@dimen/material_component_text_fields_padding_above_and_below_label"
|
||||
android:text="@string/no_results_found"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/latest_card"
|
||||
style="@style/Theme.Widget.CardView.Item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="144dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/latest_progress"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/latest_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
tools:listitem="@layout/global_search_controller_comfortable_card_item" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
@ -157,6 +157,13 @@
|
||||
<string name="library_group_updates_all_but_ungrouped">Launch global updates only for ungrouped, category updates for others</string>
|
||||
<string name="library_group_updates_all">Launch category updates all the time</string>
|
||||
|
||||
<!-- Browse settings -->
|
||||
<string name="pref_latest_tab_language_code">Display language code next to name</string>
|
||||
<string name="pref_latest_position">Latest tab position</string>
|
||||
<string name="pref_latest_position_summery">Do you want the latest tab to be the first tab in browse? This will make it the default tab when opening browse, not recommended if your on data or a metered network</string>
|
||||
<string name="pref_source_navigation">Replace latest button</string>
|
||||
<string name="pref_source_navigation_summery">Replace latest button with a custom browse view that includes both latest and browse</string>
|
||||
|
||||
<!-- Reader Settings -->
|
||||
<string name="download_threads">Download threads</string>
|
||||
<string name="download_threads_summary">Higher values can speed up image downloading significantly, but can also trigger bans. Recommended value is 2 or 3. Current value is: %s</string>
|
||||
@ -234,9 +241,6 @@
|
||||
<string name="unwatch">Unwatch</string>
|
||||
<string name="too_many_watched">Too many watched sources, cannot add more then 5</string>
|
||||
<string name="latest_tab_empty">You don\'t have any watched sources, go to the sources tab and long press a source to watch it</string>
|
||||
<string name="pref_latest_tab_language_code">Display language code next to name</string>
|
||||
<string name="pref_latest_position">Latest tab position</string>
|
||||
<string name="pref_latest_position_summery">Do you want the latest tab to be the first tab in browse? This will make it the default tab when opening browse, not recommended if your on data or a metered network</string>
|
||||
|
||||
<!-- Extension section -->
|
||||
<string name="ext_redundant">Redundant</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user