Migrate Source Filter Screen to Compose (#7031)

* Migrate Source Filter Screen to Compose

* Changes from Review and some more fixes

* Rename some variable and classes

* Review Change

* Ewbase and Review changes
This commit is contained in:
FourTOne5 2022-04-30 08:49:02 -07:00 committed by GitHub
parent cccd09fb5c
commit 23f8f35354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 324 additions and 123 deletions

View File

@ -19,6 +19,12 @@ class SourceRepositoryImpl(
} }
} }
override fun getOnlineSources(): Flow<List<Source>> {
return sourceManager.onlineSources.map { sources ->
sources.map(sourceMapper)
}
}
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> { override fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> {
val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() } val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
return sourceIdWithFavoriteCount.map { sourceIdsWithCount -> return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->

View File

@ -11,10 +11,12 @@ import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.domain.source.interactor.DisableSource
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.repository.SourceRepository
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
@ -37,10 +39,12 @@ class DomainModule : InjektModule {
addFactory { RemoveHistoryByMangaId(get()) } addFactory { RemoveHistoryByMangaId(get()) }
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) } addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
addFactory { GetLanguagesWithSources(get(), get()) }
addFactory { GetEnabledSources(get(), get()) } addFactory { GetEnabledSources(get(), get()) }
addFactory { DisableSource(get()) } addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) } addFactory { ToggleSourcePin(get()) }
addFactory { GetSourcesWithFavoriteCount(get(), get()) } addFactory { GetSourcesWithFavoriteCount(get(), get()) }
addFactory { SetMigrateSorting(get()) } addFactory { SetMigrateSorting(get()) }
addFactory { ToggleLanguage(get()) }
} }
} }

View File

@ -1,14 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.model.Source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.preference.plusAssign
class DisableSource(
private val preferences: PreferencesHelper
) {
fun await(source: Source) {
preferences.disabledSources() += source.id.toString()
}
}

View File

@ -0,0 +1,35 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
class GetLanguagesWithSources(
private val repository: SourceRepository,
private val preferences: PreferencesHelper,
) {
fun subscribe(): Flow<Map<String, List<Source>>> {
return combine(
preferences.enabledLanguages().asFlow(),
preferences.disabledSources().asFlow(),
repository.getOnlineSources()
) { enabledLanguage, disabledSource, onlineSources ->
val sortedSources = onlineSources.sortedWith(
compareBy<Source> { it.id.toString() in disabledSource }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }
)
sortedSources.groupBy { it.lang }
.toSortedMap(
compareBy(
{ it !in enabledLanguage },
{ LocaleHelper.getDisplayName(it) }
)
)
}
}
}

View File

@ -0,0 +1,19 @@
package eu.kanade.domain.source.interactor
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign
class ToggleLanguage(
val preferences: PreferencesHelper
) {
fun await(language: String) {
val isEnabled = language in preferences.enabledLanguages().get()
if (isEnabled) {
preferences.enabledLanguages() -= language
} else {
preferences.enabledLanguages() += language
}
}
}

View File

@ -0,0 +1,20 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.model.Source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign
class ToggleSource(
private val preferences: PreferencesHelper
) {
fun await(source: Source) {
val isEnabled = source.id.toString() !in preferences.disabledSources().get()
if (isEnabled) {
preferences.disabledSources() += source.id.toString()
} else {
preferences.disabledSources() -= source.id.toString()
}
}
}

View File

@ -7,5 +7,7 @@ interface SourceRepository {
fun getSources(): Flow<List<Source>> fun getSources(): Flow<List<Source>>
fun getOnlineSources(): Flow<List<Source>>
fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
} }

View File

@ -0,0 +1,130 @@
package eu.kanade.presentation.source
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.source.components.BaseSourceItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
import eu.kanade.tachiyomi.ui.browse.source.SourceFilterPresenter
import eu.kanade.tachiyomi.ui.browse.source.SourceFilterState
import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
fun SourceFilterScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: SourceFilterPresenter,
onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit
) {
val state by presenter.state.collectAsState()
when (state) {
is SourceFilterState.Loading -> LoadingScreen()
is SourceFilterState.Error -> Text(text = (state as SourceFilterState.Error).error!!.message!!)
is SourceFilterState.Success ->
SourceFilterContent(
nestedScrollInterop = nestedScrollInterop,
items = (state as SourceFilterState.Success).models,
onClickLang = onClickLang,
onClickSource = onClickSource,
)
}
}
@Composable
fun SourceFilterContent(
nestedScrollInterop: NestedScrollConnection,
items: List<FilterUiModel>,
onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit
) {
if (items.isEmpty()) {
EmptyScreen(textResource = R.string.source_filter_empty_screen)
return
}
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop)
) {
items(
items = items,
contentType = {
when (it) {
is FilterUiModel.Header -> "header"
is FilterUiModel.Item -> "item"
}
},
key = {
when (it) {
is FilterUiModel.Header -> it.hashCode()
is FilterUiModel.Item -> it.source.key()
}
}
) { model ->
when (model) {
is FilterUiModel.Header -> {
SourceFilterHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
isEnabled = model.isEnabled,
onClickItem = onClickLang
)
}
is FilterUiModel.Item -> SourceFilterItem(
modifier = Modifier.animateItemPlacement(),
source = model.source,
isEnabled = model.isEnabled,
onClickItem = onClickSource
)
}
}
}
}
@Composable
fun SourceFilterHeader(
modifier: Modifier,
language: String,
isEnabled: Boolean,
onClickItem: (String) -> Unit
) {
PreferenceRow(
modifier = modifier,
title = LocaleHelper.getSourceDisplayName(language, LocalContext.current),
action = {
Switch(checked = isEnabled, onCheckedChange = null)
},
onClick = { onClickItem(language) },
)
}
@Composable
fun SourceFilterItem(
modifier: Modifier,
source: Source,
isEnabled: Boolean,
onClickItem: (Source) -> Unit
) {
BaseSourceItem(
modifier = modifier,
source = source,
showLanguageInContent = false,
onClickItem = { onClickItem(source) },
action = {
Checkbox(checked = isEnabled, onCheckedChange = null)
}
)
}

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import rx.Observable import rx.Observable
@ -18,6 +19,8 @@ open class SourceManager(private val context: Context) {
private val _catalogueSources: MutableStateFlow<List<CatalogueSource>> = MutableStateFlow(listOf()) private val _catalogueSources: MutableStateFlow<List<CatalogueSource>> = MutableStateFlow(listOf())
val catalogueSources: Flow<List<CatalogueSource>> = _catalogueSources val catalogueSources: Flow<List<CatalogueSource>> = _catalogueSources
val onlineSources: Flow<List<HttpSource>> =
_catalogueSources.map { sources -> sources.filterIsInstance<HttpSource>() }
init { init {
createInternalSources().forEach { registerSource(it) } createInternalSources().forEach { registerSource(it) }

View File

@ -48,7 +48,7 @@ class SourceController : SearchableComposeController<SourcePresenter>() {
openSource(source, BrowseSourceController(source)) openSource(source, BrowseSourceController(source))
}, },
onClickDisable = { source -> onClickDisable = { source ->
presenter.disableSource(source) presenter.toggleSource(source)
}, },
onClickLatest = { source -> onClickLatest = { source ->
openSource(source, LatestUpdatesController(source)) openSource(source, LatestUpdatesController(source))

View File

@ -1,112 +1,34 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import android.graphics.drawable.Drawable import androidx.compose.runtime.Composable
import androidx.preference.CheckBoxPreference import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.preference.PreferenceGroup import eu.kanade.domain.source.model.Source
import androidx.preference.PreferenceScreen import eu.kanade.presentation.source.SourceFilterScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreferenceCategory
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.TreeMap
class SourceFilterController : SettingsController() { class SourceFilterController : ComposeController<SourceFilterPresenter>() {
private val onlineSources by lazy { Injekt.get<SourceManager>().getOnlineSources() } override fun getTitle() = resources?.getString(R.string.label_sources)
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { override fun createPresenter(): SourceFilterPresenter = SourceFilterPresenter()
titleRes = R.string.label_sources
// Get the list of active language codes. @Composable
val activeLangsCodes = preferences.enabledLanguages().get() override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
SourceFilterScreen(
// Get a map of sources grouped by language. nestedScrollInterop = nestedScrollInterop,
val sourcesByLang = onlineSources.groupByTo(TreeMap(), { it.lang }) presenter = presenter,
onClickLang = { language ->
// Order first by active languages, then inactive ones presenter.toggleLanguage(language)
val orderedLangs = sourcesByLang.keys.sortedWith( },
compareBy( onClickSource = { source ->
{ it !in activeLangsCodes }, presenter.toggleSource(source)
{ LocaleHelper.getSourceDisplayName(it, context) }, },
),
) )
orderedLangs.forEach { lang ->
val sources = sourcesByLang[lang].orEmpty().sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
// Create a preference group and set initial state and change listener
switchPreferenceCategory {
this@apply.addPreference(this)
title = LocaleHelper.getSourceDisplayName(lang, context)
isPersistent = false
if (lang in activeLangsCodes) {
setChecked(true)
addLanguageSources(this, sources)
}
onChange { newValue ->
val checked = newValue as Boolean
if (!checked) {
preferences.enabledLanguages() -= lang
removeAll()
} else {
preferences.enabledLanguages() += lang
addLanguageSources(this, sources)
}
true
}
}
}
}
override fun setDivider(divider: Drawable?) {
super.setDivider(null)
}
/**
* Adds the source list for the given group (language).
*
* @param group the language category.
*/
private fun addLanguageSources(group: PreferenceGroup, sources: List<HttpSource>) {
val disabledSourceIds = preferences.disabledSources().get()
sources
.sortedBy { it.id.toString() in disabledSourceIds }
.map { source ->
CheckBoxPreference(group.context).apply {
val id = source.id.toString()
title = source.name
key = source.getPreferenceKey()
isPersistent = false
isChecked = id !in disabledSourceIds
val sourceIcon = source.icon()
if (sourceIcon != null) {
icon = sourceIcon
}
onChange { newValue ->
val checked = newValue as Boolean
if (checked) {
preferences.disabledSources() -= id
} else {
preferences.disabledSources() += id
}
true
}
}
}
.forEach { group.addPreference(it) }
} }
} }
sealed class FilterUiModel {
data class Header(val language: String, val isEnabled: Boolean) : FilterUiModel()
data class Item(val source: Source, val isEnabled: Boolean) : FilterUiModel()
}

View File

@ -0,0 +1,71 @@
package eu.kanade.tachiyomi.ui.browse.source
import android.os.Bundle
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.model.Source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SourceFilterPresenter(
private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<SourceFilterController>() {
private val _state: MutableStateFlow<SourceFilterState> = MutableStateFlow(SourceFilterState.Loading)
val state: StateFlow<SourceFilterState> = _state.asStateFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
getLanguagesWithSources.subscribe()
.catch { exception ->
_state.emit(SourceFilterState.Error(exception))
}
.collectLatest { sourceLangMap ->
val uiModels = sourceLangMap.toFilterUiModels()
_state.emit(SourceFilterState.Success(uiModels))
}
}
}
private fun Map<String, List<Source>>.toFilterUiModels(): List<FilterUiModel> {
return this.flatMap {
val isLangEnabled = it.key in preferences.enabledLanguages().get()
val header = listOf(FilterUiModel.Header(it.key, isLangEnabled))
if (isLangEnabled.not()) return@flatMap header
header + it.value.map { source ->
FilterUiModel.Item(
source,
source.id.toString() !in preferences.disabledSources().get()
)
}
}
}
fun toggleSource(source: Source) {
toggleSource.await(source)
}
fun toggleLanguage(language: String) {
toggleLanguage.await(language)
}
}
sealed class SourceFilterState {
object Loading : SourceFilterState()
data class Error(val error: Throwable) : SourceFilterState()
data class Success(val models: List<FilterUiModel>) : SourceFilterState()
}

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import android.os.Bundle import android.os.Bundle
import eu.kanade.domain.source.interactor.DisableSource
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
@ -24,7 +24,7 @@ import java.util.TreeMap
*/ */
class SourcePresenter( class SourcePresenter(
private val getEnabledSources: GetEnabledSources = Injekt.get(), private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val disableSource: DisableSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get() private val toggleSourcePin: ToggleSourcePin = Injekt.get()
) : BasePresenter<SourceController>() { ) : BasePresenter<SourceController>() {
@ -79,8 +79,8 @@ class SourcePresenter(
} }
} }
fun disableSource(source: Source) { fun toggleSource(source: Source) {
disableSource.await(source) toggleSource.await(source)
} }
fun togglePin(source: Source) { fun togglePin(source: Source) {

View File

@ -711,6 +711,9 @@
<string name="clear_history_completed">History deleted</string> <string name="clear_history_completed">History deleted</string>
<string name="clear_history_confirmation">Are you sure? All history will be lost.</string> <string name="clear_history_confirmation">Are you sure? All history will be lost.</string>
<!-- Source Filter Screen -->
<string name="source_filter_empty_screen">No installed source found</string>
<!-- Source migration screen --> <!-- Source migration screen -->
<string name="migration_help_guide">Source migration guide</string> <string name="migration_help_guide">Source migration guide</string>
<string name="migration_dialog_what_to_include">Select data to include</string> <string name="migration_dialog_what_to_include">Select data to include</string>