mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-14 13:08:56 +01:00
Convert Source tab to use Compose (#6987)
* Use Compose in Source tab * Replace hashCode with key function * Add ability to turn off pins moving on top of source list * Changes from review comments
This commit is contained in:
@@ -7,6 +7,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import nucleus.presenter.Presenter
|
||||
|
||||
/**
|
||||
@@ -52,3 +53,22 @@ abstract class BasicComposeController : BaseController<ComposeControllerBinding>
|
||||
|
||||
@Composable abstract fun ComposeContent(nestedScrollInterop: NestedScrollConnection)
|
||||
}
|
||||
|
||||
abstract class SearchableComposeController<P : BasePresenter<*>> : SearchableNucleusController<ComposeControllerBinding, P>() {
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
|
||||
ComposeControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.root.setContent {
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection(binding.root)
|
||||
TachiyomiTheme {
|
||||
ComposeContent(nestedScrollInterop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable abstract fun ComposeContent(nestedScrollInterop: NestedScrollConnection)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = SectionHeaderItemBinding.bind(view)
|
||||
|
||||
fun bind(item: LangItem) {
|
||||
binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* Item that contains the language header.
|
||||
*
|
||||
* @param code The lang code.
|
||||
*/
|
||||
data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.section_header_item
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LangHolder {
|
||||
return LangHolder(view, adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds this item to the given view holder.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: LangHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>,
|
||||
) {
|
||||
holder.bind(this)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
|
||||
/**
|
||||
* Adapter that holds the catalogue cards.
|
||||
*
|
||||
* @param controller instance of [SourceController].
|
||||
*/
|
||||
class SourceAdapter(controller: SourceController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for browse item clicks.
|
||||
*/
|
||||
val clickListener: OnSourceClickListener = controller
|
||||
|
||||
/**
|
||||
* Listener which should be called when user clicks browse.
|
||||
* Note: Should only be handled by [SourceController]
|
||||
*/
|
||||
interface OnSourceClickListener {
|
||||
fun onBrowseClick(position: Int)
|
||||
fun onLatestClick(position: Int)
|
||||
fun onPinClick(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -1,182 +1,76 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.presentation.source.SourceScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
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.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This controller shows and manages the different catalogues enabled by the user.
|
||||
* This controller should only handle UI actions, IO actions should be done by [SourcePresenter]
|
||||
* [SourceAdapter.OnSourceClickListener] call function data on browse item click.
|
||||
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||
*/
|
||||
class SourceController :
|
||||
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
SourceAdapter.OnSourceClickListener {
|
||||
class SourceController : SearchableComposeController<SourcePresenter>() {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var adapter: SourceAdapter? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.label_sources)
|
||||
}
|
||||
override fun getTitle(): String? =
|
||||
resources?.getString(R.string.label_sources)
|
||||
|
||||
override fun createPresenter(): SourcePresenter {
|
||||
return SourcePresenter()
|
||||
}
|
||||
override fun createPresenter(): SourcePresenter =
|
||||
SourcePresenter()
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = SourceMainControllerBinding.inflate(inflater)
|
||||
@Composable
|
||||
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
|
||||
SourceScreen(
|
||||
nestedScrollInterop = nestedScrollInterop,
|
||||
presenter = presenter,
|
||||
onClickItem = { source ->
|
||||
openSource(source, BrowseSourceController(source))
|
||||
},
|
||||
onClickDisable = { source ->
|
||||
presenter.disableSource(source)
|
||||
},
|
||||
onClickLatest = { source ->
|
||||
openSource(source, LatestUpdatesController(source))
|
||||
},
|
||||
onClickPin = { source ->
|
||||
presenter.togglePin(source)
|
||||
},
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = SourceAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.onAnimationsFinished {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
|
||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||
|
||||
// Update list on extension changes (e.g. new installation)
|
||||
(parentController as BrowseController).extensionListUpdateRelay
|
||||
.skip(1) // Skip first update when ExtensionController created
|
||||
.subscribeUntilDestroy {
|
||||
presenter.updateSources()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (type.isPush) {
|
||||
presenter.updateSources()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
onItemClick(position)
|
||||
return false
|
||||
}
|
||||
|
||||
private fun onItemClick(position: Int) {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
val source = item.source
|
||||
openSource(source, BrowseSourceController(source))
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
|
||||
val isPinned = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
|
||||
|
||||
val items = mutableListOf(
|
||||
activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin) to { toggleSourcePin(item.source) },
|
||||
)
|
||||
if (item.source !is LocalSource) {
|
||||
items.add(activity.getString(R.string.action_disable) to { disableSource(item.source) })
|
||||
}
|
||||
|
||||
SourceOptionsDialog(item.source.toString(), items).showDialog(router)
|
||||
}
|
||||
|
||||
private fun disableSource(source: Source) {
|
||||
preferences.disabledSources() += source.id.toString()
|
||||
|
||||
presenter.updateSources()
|
||||
}
|
||||
|
||||
private fun toggleSourcePin(source: Source) {
|
||||
val isPinned = source.id.toString() in preferences.pinnedSources().get()
|
||||
if (isPinned) {
|
||||
preferences.pinnedSources() -= source.id.toString()
|
||||
} else {
|
||||
preferences.pinnedSources() += source.id.toString()
|
||||
}
|
||||
|
||||
presenter.updateSources()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when browse is clicked in [SourceAdapter]
|
||||
*/
|
||||
override fun onBrowseClick(position: Int) {
|
||||
onItemClick(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when latest is clicked in [SourceAdapter]
|
||||
*/
|
||||
override fun onLatestClick(position: Int) {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
openSource(item.source, LatestUpdatesController(item.source))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when pin icon is clicked in [SourceAdapter]
|
||||
*/
|
||||
override fun onPinClick(position: Int) {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
toggleSourcePin(item.source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a catalogue with the given controller.
|
||||
*/
|
||||
private fun openSource(source: CatalogueSource, controller: BrowseSourceController) {
|
||||
private fun openSource(source: Source, controller: BrowseSourceController) {
|
||||
if (!preferences.incognitoMode().get()) {
|
||||
preferences.lastUsedSource().set(source.id)
|
||||
}
|
||||
@@ -190,51 +84,13 @@ class SourceController :
|
||||
* @return True if this event has been consumed, false if it has not.
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
return when (item.itemId) {
|
||||
// Initialize option to open catalogue settings.
|
||||
R.id.action_settings -> {
|
||||
parentController!!.router.pushController(SourceFilterController())
|
||||
true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update adapter containing sources.
|
||||
*/
|
||||
fun setSources(sources: List<IFlexible<*>>) {
|
||||
adapter?.updateDataSet(sources)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to set the last used catalogue at the top of the view.
|
||||
*/
|
||||
fun setLastUsedSource(item: SourceItem?) {
|
||||
adapter?.removeAllScrollableHeaders()
|
||||
if (item != null) {
|
||||
adapter?.addScrollableHeader(item)
|
||||
adapter?.addScrollableHeader(LangItem(SourcePresenter.LAST_USED_KEY))
|
||||
}
|
||||
}
|
||||
|
||||
class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
private lateinit var source: String
|
||||
private lateinit var items: List<Pair<String, () -> Unit>>
|
||||
|
||||
constructor(source: String, items: List<Pair<String, () -> Unit>>) : this() {
|
||||
this.source = source
|
||||
this.items = items
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(source)
|
||||
.setItems(items.map { it.first }.toTypedArray()) { dialog, which ->
|
||||
items[which].second()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
|
||||
class SourceHolder(view: View, val adapter: SourceAdapter) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = SourceMainControllerItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.sourceLatest.setOnClickListener {
|
||||
adapter.clickListener.onLatestClick(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
binding.pin.setOnClickListener {
|
||||
adapter.clickListener.onPinClick(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: SourceItem) {
|
||||
val source = item.source
|
||||
|
||||
binding.title.text = source.name
|
||||
binding.subtitle.isVisible = source !is LocalSource
|
||||
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
|
||||
|
||||
// Set source icon
|
||||
val icon = source.icon()
|
||||
when {
|
||||
icon != null -> binding.image.load(icon)
|
||||
item.source.id == LocalSource.ID -> binding.image.load(R.mipmap.ic_local_source)
|
||||
}
|
||||
|
||||
binding.sourceLatest.isVisible = source.supportsLatest
|
||||
|
||||
binding.pin.isVisible = true
|
||||
if (item.isPinned) {
|
||||
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, R.attr.colorAccent)
|
||||
} else {
|
||||
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, android.R.attr.textColorHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
|
||||
/**
|
||||
* Item that contains source information.
|
||||
*
|
||||
* @param source Instance of [CatalogueSource] containing source information.
|
||||
* @param header The header for this item.
|
||||
*/
|
||||
data class SourceItem(
|
||||
val source: CatalogueSource,
|
||||
val header: LangItem? = null,
|
||||
val isPinned: Boolean = false,
|
||||
) :
|
||||
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.source_main_controller_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
||||
return SourceHolder(view, adapter as SourceAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: SourceHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>,
|
||||
) {
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is SourceItem) {
|
||||
return source.id == other.source.id &&
|
||||
getHeader()?.code == other.getHeader()?.code &&
|
||||
isPinned == other.isPinned
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = source.id.hashCode()
|
||||
result = 31 * result + (header?.hashCode() ?: 0)
|
||||
result = 31 * result + isPinned.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import android.os.Bundle
|
||||
import eu.kanade.domain.source.interactor.DisableSource
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||
import eu.kanade.domain.source.model.Pin
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.TreeMap
|
||||
@@ -20,87 +23,68 @@ import java.util.TreeMap
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*/
|
||||
class SourcePresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val getEnabledSources: GetEnabledSources = Injekt.get(),
|
||||
private val disableSource: DisableSource = Injekt.get(),
|
||||
private val toggleSourcePin: ToggleSourcePin = Injekt.get()
|
||||
) : BasePresenter<SourceController>() {
|
||||
|
||||
var sources = getEnabledSources()
|
||||
private val _state: MutableStateFlow<SourceState> = MutableStateFlow(SourceState.EMPTY)
|
||||
val state: StateFlow<SourceState> = _state.asStateFlow()
|
||||
|
||||
/**
|
||||
* Unsubscribe and create a new subscription to fetch enabled sources.
|
||||
*/
|
||||
private fun loadSources() {
|
||||
val pinnedSources = mutableListOf<SourceItem>()
|
||||
val pinnedSourceIds = preferences.pinnedSources().get()
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
presenterScope.launchIO {
|
||||
getEnabledSources.subscribe()
|
||||
.catch { exception ->
|
||||
_state.update { state ->
|
||||
state.copy(sources = listOf(), error = exception)
|
||||
}
|
||||
}
|
||||
.collectLatest(::collectLatestSources)
|
||||
}
|
||||
}
|
||||
|
||||
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
|
||||
private suspend fun collectLatestSources(sources: List<Source>) {
|
||||
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
|
||||
// Catalogues without a lang defined will be placed at the end
|
||||
when {
|
||||
d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1
|
||||
d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1
|
||||
d1 == PINNED_KEY && d2 != PINNED_KEY -> -1
|
||||
d2 == PINNED_KEY && d1 != PINNED_KEY -> 1
|
||||
d1 == "" && d2 != "" -> 1
|
||||
d2 == "" && d1 != "" -> -1
|
||||
else -> d1.compareTo(d2)
|
||||
}
|
||||
}
|
||||
val byLang = sources.groupByTo(map) { it.lang }
|
||||
var sourceItems = byLang.flatMap {
|
||||
val langItem = LangItem(it.key)
|
||||
it.value.map { source ->
|
||||
val isPinned = source.id.toString() in pinnedSourceIds
|
||||
if (isPinned) {
|
||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), isPinned))
|
||||
}
|
||||
|
||||
SourceItem(source, langItem, isPinned)
|
||||
val byLang = sources.groupByTo(map) {
|
||||
when {
|
||||
it.isUsedLast -> LAST_USED_KEY
|
||||
Pin.Actual in it.pin -> PINNED_KEY
|
||||
else -> it.lang
|
||||
}
|
||||
}
|
||||
|
||||
if (pinnedSources.isNotEmpty()) {
|
||||
sourceItems = pinnedSources + sourceItems
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
sources = byLang.flatMap {
|
||||
listOf(
|
||||
UiModel.Header(it.key),
|
||||
*it.value.map { source ->
|
||||
UiModel.Item(source)
|
||||
}.toTypedArray()
|
||||
)
|
||||
},
|
||||
error = null
|
||||
)
|
||||
}
|
||||
|
||||
view?.setSources(sourceItems)
|
||||
}
|
||||
|
||||
private fun loadLastUsedSource() {
|
||||
// Immediate initial load
|
||||
preferences.lastUsedSource().get().let { updateLastUsedSource(it) }
|
||||
|
||||
// Subsequent updates
|
||||
preferences.lastUsedSource().asFlow()
|
||||
.drop(1)
|
||||
.onStart { delay(500) }
|
||||
.distinctUntilChanged()
|
||||
.onEach { updateLastUsedSource(it) }
|
||||
.launchIn(presenterScope)
|
||||
fun disableSource(source: Source) {
|
||||
disableSource.await(source)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
source?.let { view?.setLastUsedSource(it) }
|
||||
}
|
||||
|
||||
fun updateSources() {
|
||||
sources = getEnabledSources()
|
||||
loadSources()
|
||||
loadLastUsedSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of enabled sources ordered by language and name.
|
||||
*
|
||||
* @return list containing enabled sources.
|
||||
*/
|
||||
private fun getEnabledSources(): List<CatalogueSource> {
|
||||
val languages = preferences.enabledLanguages().get()
|
||||
val disabledSourceIds = preferences.disabledSources().get()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in languages || it.id == LocalSource.ID }
|
||||
.filterNot { it.id.toString() in disabledSourceIds }
|
||||
.sortedBy { "(${it.lang}) ${it.name.lowercase()}" }
|
||||
fun togglePin(source: Source) {
|
||||
toggleSourcePin.await(source)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -108,3 +92,27 @@ class SourcePresenter(
|
||||
const val LAST_USED_KEY = "last_used"
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UiModel {
|
||||
data class Item(val source: Source) : UiModel()
|
||||
data class Header(val language: String) : UiModel()
|
||||
}
|
||||
|
||||
data class SourceState(
|
||||
val sources: List<UiModel>,
|
||||
val error: Throwable?
|
||||
) {
|
||||
|
||||
val isLoading: Boolean
|
||||
get() = sources.isEmpty() && error == null
|
||||
|
||||
val hasError: Boolean
|
||||
get() = error != null
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = sources.isEmpty()
|
||||
|
||||
companion object {
|
||||
val EMPTY = SourceState(listOf(), null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -69,16 +70,19 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
ChangeMangaCategoriesDialog.Listener {
|
||||
|
||||
constructor(source: CatalogueSource, searchQuery: String? = null) : this(
|
||||
constructor(sourceId: Long, query: String? = null) : this(
|
||||
Bundle().apply {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
|
||||
if (searchQuery != null) {
|
||||
putString(SEARCH_QUERY_KEY, searchQuery)
|
||||
putLong(SOURCE_ID_KEY, sourceId)
|
||||
query?.let { query ->
|
||||
putString(SEARCH_QUERY_KEY, query)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
constructor(source: CatalogueSource, query: String? = null) : this(source.id, query)
|
||||
|
||||
constructor(source: Source, query: String? = null) : this(source.id, query)
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.source.latest
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import androidx.core.os.bundleOf
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
|
||||
@@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
*/
|
||||
class LatestUpdatesController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
|
||||
constructor(source: CatalogueSource) : this(
|
||||
constructor(source: Source) : this(
|
||||
bundleOf(SOURCE_ID_KEY to source.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,17 @@ class SettingsBrowseController : SettingsController() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
||||
titleRes = R.string.browse
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pref_category_general
|
||||
|
||||
switchPreference {
|
||||
bindTo(preferences.pinsOnTop())
|
||||
titleRes = R.string.pref_move_on_top
|
||||
summaryRes = R.string.pref_move_on_top_summary
|
||||
defaultValue = true
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.label_extensions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user