Эх сурвалжийг харах

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
FourTOne5 2 жил өмнө
parent
commit
23f8f35354

+ 6 - 0
app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt

@@ -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>>> {
         val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
         return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->

+ 6 - 2
app/src/main/java/eu/kanade/domain/DomainModule.kt

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

+ 0 - 14
app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt

@@ -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()
-    }
-}

+ 35 - 0
app/src/main/java/eu/kanade/domain/source/interactor/GetLanguagesWithSources.kt

@@ -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) }
+                    )
+                )
+        }
+    }
+}

+ 19 - 0
app/src/main/java/eu/kanade/domain/source/interactor/ToggleLanguage.kt

@@ -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
+        }
+    }
+}

+ 20 - 0
app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt

@@ -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()
+        }
+    }
+}

+ 2 - 0
app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt

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

+ 130 - 0
app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt

@@ -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)
+        }
+    )
+}

+ 3 - 0
app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt

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

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt

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

+ 23 - 101
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt

@@ -1,112 +1,34 @@
 package eu.kanade.tachiyomi.ui.browse.source
 
-import android.graphics.drawable.Drawable
-import androidx.preference.CheckBoxPreference
-import androidx.preference.PreferenceGroup
-import androidx.preference.PreferenceScreen
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import eu.kanade.domain.source.model.Source
+import eu.kanade.presentation.source.SourceFilterScreen
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.SourceManager
-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
+import eu.kanade.tachiyomi.ui.base.controller.ComposeController
 
-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 {
-        titleRes = R.string.label_sources
+    override fun createPresenter(): SourceFilterPresenter = SourceFilterPresenter()
 
-        // Get the list of active language codes.
-        val activeLangsCodes = preferences.enabledLanguages().get()
-
-        // Get a map of sources grouped by language.
-        val sourcesByLang = onlineSources.groupByTo(TreeMap(), { it.lang })
-
-        // Order first by active languages, then inactive ones
-        val orderedLangs = sourcesByLang.keys.sortedWith(
-            compareBy(
-                { it !in activeLangsCodes },
-                { LocaleHelper.getSourceDisplayName(it, context) },
-            ),
+    @Composable
+    override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
+        SourceFilterScreen(
+            nestedScrollInterop = nestedScrollInterop,
+            presenter = presenter,
+            onClickLang = { language ->
+                presenter.toggleLanguage(language)
+            },
+            onClickSource = { source ->
+                presenter.toggleSource(source)
+            },
         )
-
-        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 {
-                [email protected](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()
 }

+ 71 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt

@@ -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()
+}

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt

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

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -711,6 +711,9 @@
     <string name="clear_history_completed">History deleted</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 -->
     <string name="migration_help_guide">Source migration guide</string>
     <string name="migration_dialog_what_to_include">Select data to include</string>