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 continuousVerticalTappingByPage = "continuous_vertical_tapping_by_page"
|
||||||
|
|
||||||
const val groupLibraryUpdateType = "group_library_update_type"
|
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 continuousVerticalTappingByPage() = flowPrefs.getBoolean(Keys.continuousVerticalTappingByPage, false)
|
||||||
|
|
||||||
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
|
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.SourceDividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
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.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.browse.source.latest.LatestUpdatesController
|
||||||
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@ -152,7 +153,9 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
when (mode) {
|
when (mode) {
|
||||||
Mode.CATALOGUE -> {
|
Mode.CATALOGUE -> {
|
||||||
// Open the catalogue view.
|
// 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(
|
Mode.SMART_SEARCH -> router.pushController(
|
||||||
SmartSearchController(
|
SmartSearchController(
|
||||||
@ -319,6 +322,16 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
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.
|
* 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.source_latest
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
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),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
SourceListItem,
|
SourceListItem,
|
||||||
SlicedHolder {
|
SlicedHolder {
|
||||||
@ -39,7 +39,7 @@ class SourceHolder(private val view: View, override val adapter: SourceAdapter /
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (!showButtons) {
|
if (!showLatest) {
|
||||||
source_latest.isVisible = false
|
source_latest.isVisible = false
|
||||||
}
|
}
|
||||||
// SY <--
|
// 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) {
|
if (item.isPinned) {
|
||||||
pin.setVectorCompat(R.drawable.ic_push_pin_filled_24dp, view.context.getResourceColor(R.attr.colorAccent))
|
pin.setVectorCompat(R.drawable.ic_push_pin_filled_24dp, view.context.getResourceColor(R.attr.colorAccent))
|
||||||
} else {
|
} else {
|
||||||
|
@ -19,7 +19,8 @@ data class SourceItem(
|
|||||||
val header: LangItem? = null,
|
val header: LangItem? = null,
|
||||||
val isPinned: Boolean = false,
|
val isPinned: Boolean = false,
|
||||||
// SY -->
|
// SY -->
|
||||||
val showButtons: Boolean
|
val showLatest: Boolean,
|
||||||
|
val showPins: Boolean
|
||||||
// SY <--
|
// SY <--
|
||||||
) :
|
) :
|
||||||
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
||||||
@ -35,7 +36,7 @@ data class SourceItem(
|
|||||||
* Creates a new view holder for this item.
|
* Creates a new view holder for this item.
|
||||||
*/
|
*/
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
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 {
|
var sourceItems = byLang.flatMap {
|
||||||
val langItem = LangItem(it.key)
|
val langItem = LangItem(it.key)
|
||||||
it.value.map { source ->
|
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
|
val isPinned = source.id.toString() in pinnedSourceIds
|
||||||
if (isPinned) {
|
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 -->
|
// SY -->
|
||||||
@ -108,7 +112,8 @@ class SourcePresenter(
|
|||||||
source,
|
source,
|
||||||
LangItem("custom|" + SourceAndCategory.second),
|
LangItem("custom|" + SourceAndCategory.second),
|
||||||
isPinned,
|
isPinned,
|
||||||
controllerMode == SourceController.Mode.CATALOGUE
|
showLatest,
|
||||||
|
showPins
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -117,7 +122,7 @@ class SourcePresenter(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// 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) {
|
private fun updateLastUsedSource(sourceId: Long) {
|
||||||
val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let {
|
val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let {
|
||||||
val isPinned = it.id.toString() in preferences.pinnedSources().get()
|
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) }
|
source?.let { view?.setLastUsedSource(it) }
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
source: CatalogueSource,
|
source: CatalogueSource,
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
// SY -->
|
// SY -->
|
||||||
smartSearchConfig: SourceController.SmartSearchConfig? = null
|
smartSearchConfig: SourceController.SmartSearchConfig? = null,
|
||||||
|
filterList: String? = null
|
||||||
// SY <--
|
// SY <--
|
||||||
) : this(
|
) : this(
|
||||||
Bundle().apply {
|
Bundle().apply {
|
||||||
@ -96,6 +97,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
if (smartSearchConfig != null) {
|
if (smartSearchConfig != null) {
|
||||||
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
|
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filterList != null) {
|
||||||
|
putString(FILTERS_CONFIG_KEY, filterList)
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -160,7 +165,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
return BrowseSourcePresenter(
|
return BrowseSourcePresenter(
|
||||||
args.getLong(SOURCE_ID_KEY),
|
args.getLong(SOURCE_ID_KEY),
|
||||||
args.getString(SEARCH_QUERY_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 <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@ -851,6 +857,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
||||||
|
const val FILTERS_CONFIG_KEY = "filters"
|
||||||
const val RECOMMENDS_CONFIG = "RECOMMENDS_CONFIG"
|
const val RECOMMENDS_CONFIG = "RECOMMENDS_CONFIG"
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ open class BrowseSourcePresenter(
|
|||||||
private val searchQuery: String? = null,
|
private val searchQuery: String? = null,
|
||||||
// SY -->
|
// SY -->
|
||||||
private val recommendsMangaId: Long? = null,
|
private val recommendsMangaId: Long? = null,
|
||||||
|
private val filters: String? = null,
|
||||||
// SY <--
|
// SY <--
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
@ -115,6 +116,8 @@ open class BrowseSourcePresenter(
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private var manga: Manga? = null
|
private var manga: Manga? = null
|
||||||
|
|
||||||
|
private val filterSerializer = FilterSerializer()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,6 +132,14 @@ open class BrowseSourcePresenter(
|
|||||||
|
|
||||||
sourceFilters = source.getFilterList()
|
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) {
|
if (savedState != null) {
|
||||||
query = savedState.getString(::query.name, "")
|
query = savedState.getString(::query.name, "")
|
||||||
}
|
}
|
||||||
@ -137,7 +148,7 @@ open class BrowseSourcePresenter(
|
|||||||
manga = db.getManga(recommendsMangaId).executeAsBlocking()
|
manga = db.getManga(recommendsMangaId).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
restartPager()
|
restartPager(/* SY -->*/ filters = if (allDefault) this.appliedFilters else sourceFilters /* SY <--*/)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSave(state: Bundle) {
|
override fun onSave(state: Bundle) {
|
||||||
@ -323,48 +334,54 @@ open class BrowseSourcePresenter(
|
|||||||
return SourcePager(source, query, filters)
|
return SourcePager(source, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun FilterList.toItems(): List<IFlexible<*>> {
|
// SY -->
|
||||||
return mapNotNull { filter ->
|
companion object {
|
||||||
when (filter) {
|
// SY <--
|
||||||
is Filter.Header -> HeaderItem(filter)
|
fun FilterList.toItems(): List<IFlexible<*>> {
|
||||||
// --> EXH
|
return mapNotNull { filter ->
|
||||||
is Filter.HelpDialog -> HelpDialogItem(filter)
|
when (filter) {
|
||||||
is Filter.AutoComplete -> AutoComplete(filter)
|
is Filter.Header -> HeaderItem(filter)
|
||||||
// <-- EXH
|
// --> EXH
|
||||||
is Filter.Separator -> SeparatorItem(filter)
|
is Filter.HelpDialog -> HelpDialogItem(filter)
|
||||||
is Filter.CheckBox -> CheckboxItem(filter)
|
is Filter.AutoComplete -> AutoComplete(filter)
|
||||||
is Filter.TriState -> TriStateItem(filter)
|
// <-- EXH
|
||||||
is Filter.Text -> TextItem(filter)
|
is Filter.Separator -> SeparatorItem(filter)
|
||||||
is Filter.Select<*> -> SelectItem(filter)
|
is Filter.CheckBox -> CheckboxItem(filter)
|
||||||
is Filter.Group<*> -> {
|
is Filter.TriState -> TriStateItem(filter)
|
||||||
val group = GroupItem(filter)
|
is Filter.Text -> TextItem(filter)
|
||||||
val subItems = filter.state.mapNotNull {
|
is Filter.Select<*> -> SelectItem(filter)
|
||||||
when (it) {
|
is Filter.Group<*> -> {
|
||||||
is Filter.CheckBox -> CheckboxSectionItem(it)
|
val group = GroupItem(filter)
|
||||||
is Filter.TriState -> TriStateSectionItem(it)
|
val subItems = filter.state.mapNotNull {
|
||||||
is Filter.Text -> TextSectionItem(it)
|
when (it) {
|
||||||
is Filter.Select<*> -> SelectSectionItem(it)
|
is Filter.CheckBox -> CheckboxSectionItem(it)
|
||||||
// SY -->
|
is Filter.TriState -> TriStateSectionItem(it)
|
||||||
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
|
is Filter.Text -> TextSectionItem(it)
|
||||||
// SY <--
|
is Filter.Select<*> -> SelectSectionItem(it)
|
||||||
else -> null
|
// SY -->
|
||||||
} as? ISectionable<*, *>
|
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
|
||||||
|
// SY <--
|
||||||
|
else -> null
|
||||||
|
} as? ISectionable<*, *>
|
||||||
|
}
|
||||||
|
subItems.forEach { it.header = group }
|
||||||
|
group.subItems = subItems
|
||||||
|
group
|
||||||
}
|
}
|
||||||
subItems.forEach { it.header = group }
|
is Filter.Sort -> {
|
||||||
group.subItems = subItems
|
val group = SortGroup(filter)
|
||||||
group
|
val subItems = filter.values.map {
|
||||||
}
|
SortItem(it, group)
|
||||||
is Filter.Sort -> {
|
}
|
||||||
val group = SortGroup(filter)
|
group.subItems = subItems
|
||||||
val subItems = filter.values.map {
|
group
|
||||||
SortItem(it, group)
|
|
||||||
}
|
}
|
||||||
group.subItems = subItems
|
|
||||||
group
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// SY -->
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user categories.
|
* Get user categories.
|
||||||
@ -422,7 +439,6 @@ open class BrowseSourcePresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
private val filterSerializer = FilterSerializer()
|
|
||||||
fun saveSearches(searches: List<EXHSavedSearch>) {
|
fun saveSearches(searches: List<EXHSavedSearch>) {
|
||||||
val otherSerialized = prefs.eh_savedSearches().get().filter {
|
val otherSerialized = prefs.eh_savedSearches().get().filter {
|
||||||
!it.startsWith("${source.id}:")
|
!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())
|
router.pushController(SourceCategoryController().withFadeTransaction())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.useNewSourceNavigation
|
||||||
|
titleRes = R.string.pref_source_navigation
|
||||||
|
summaryRes = R.string.pref_source_navigation_summery
|
||||||
|
defaultValue = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
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_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>
|
<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 -->
|
<!-- Reader Settings -->
|
||||||
<string name="download_threads">Download threads</string>
|
<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>
|
<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="unwatch">Unwatch</string>
|
||||||
<string name="too_many_watched">Too many watched sources, cannot add more then 5</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="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 -->
|
<!-- Extension section -->
|
||||||
<string name="ext_redundant">Redundant</string>
|
<string name="ext_redundant">Redundant</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user