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

Migrate extension details page to Compose

arkon 2 жил өмнө
parent
commit
13943f77f7
24 өөрчлөгдсөн 363 нэмэгдсэн , 452 устгасан
  1. 2 0
      app/src/main/java/eu/kanade/domain/DomainModule.kt
  2. 32 0
      app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt
  3. 8 5
      app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt
  4. 237 0
      app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
  5. 2 2
      app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt
  6. 2 2
      app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt
  7. 3 2
      app/src/main/java/eu/kanade/presentation/browse/SourceFilterScreen.kt
  8. 2 2
      app/src/main/java/eu/kanade/presentation/browse/SourceScreen.kt
  9. 1 3
      app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt
  10. 17 164
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt
  11. 0 62
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt
  12. 50 5
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt
  13. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt
  14. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt
  15. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
  16. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt
  17. 1 0
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt
  18. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt
  19. 0 5
      app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceDSL.kt
  20. 0 34
      app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchSettingsPreference.kt
  21. 0 5
      app/src/main/res/layout/extension_detail_controller.xml
  22. 0 130
      app/src/main/res/layout/extension_detail_header.xml
  23. 0 25
      app/src/main/res/layout/pref_settings.xml
  24. 1 1
      app/src/main/res/values/strings.xml

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

@@ -3,6 +3,7 @@ package eu.kanade.domain
 import eu.kanade.data.history.HistoryRepositoryImpl
 import eu.kanade.data.manga.MangaRepositoryImpl
 import eu.kanade.data.source.SourceRepositoryImpl
+import eu.kanade.domain.extension.interactor.GetExtensionSources
 import eu.kanade.domain.extension.interactor.GetExtensionUpdates
 import eu.kanade.domain.extension.interactor.GetExtensions
 import eu.kanade.domain.history.interactor.DeleteHistoryTable
@@ -43,6 +44,7 @@ class DomainModule : InjektModule {
         addFactory { RemoveHistoryByMangaId(get()) }
 
         addFactory { GetExtensions(get(), get()) }
+        addFactory { GetExtensionSources(get()) }
         addFactory { GetExtensionUpdates(get(), get()) }
 
         addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }

+ 32 - 0
app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt

@@ -0,0 +1,32 @@
+package eu.kanade.domain.extension.interactor
+
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.extension.model.Extension
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class GetExtensionSources(
+    private val preferences: PreferencesHelper,
+) {
+
+    fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
+        val isMultiSource = extension.sources.size > 1
+        val isMultiLangSingleSource =
+            isMultiSource && extension.sources.map { it.name }.distinct().size == 1
+
+        return preferences.disabledSources().asFlow().map { disabledSources ->
+            fun Source.isEnabled() = id.toString() !in disabledSources
+
+            extension.sources
+                .map { source ->
+                    ExtensionSourceItem(
+                        source = source,
+                        enabled = source.isEnabled(),
+                        labelAsName = isMultiSource && isMultiLangSingleSource.not(),
+                    )
+                }
+        }
+    }
+}

+ 8 - 5
app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt

@@ -9,12 +9,15 @@ 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()
+    fun await(source: Source, enable: Boolean = source.id.toString() in preferences.disabledSources().get()) {
+        await(source.id, enable)
+    }
+
+    fun await(sourceId: Long, enable: Boolean = sourceId.toString() in preferences.disabledSources().get()) {
+        if (enable) {
+            preferences.disabledSources() -= sourceId.toString()
         } else {
-            preferences.disabledSources() -= source.id.toString()
+            preferences.disabledSources() += sourceId.toString()
         }
     }
 }

+ 237 - 0
app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt

@@ -0,0 +1,237 @@
+package eu.kanade.presentation.browse
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+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.Alignment
+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 androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import eu.kanade.presentation.browse.components.ExtensionIcon
+import eu.kanade.presentation.components.Divider
+import eu.kanade.presentation.components.EmptyScreen
+import eu.kanade.presentation.components.PreferenceRow
+import eu.kanade.presentation.util.horizontalPadding
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.extension.model.Extension
+import eu.kanade.tachiyomi.source.ConfigurableSource
+import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsPresenter
+import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
+import eu.kanade.tachiyomi.util.system.LocaleHelper
+
+@Composable
+fun ExtensionDetailsScreen(
+    nestedScrollInterop: NestedScrollConnection,
+    presenter: ExtensionDetailsPresenter,
+    onClickUninstall: () -> Unit,
+    onClickAppInfo: () -> Unit,
+    onClickSourcePreferences: (sourceId: Long) -> Unit,
+    onClickSource: (sourceId: Long) -> Unit,
+) {
+    val extension = presenter.extension
+
+    if (extension == null) {
+        EmptyScreen(textResource = R.string.empty_screen)
+        return
+    }
+
+    val sources by presenter.sourcesState.collectAsState()
+
+    LazyColumn(
+        modifier = Modifier.nestedScroll(nestedScrollInterop),
+        contentPadding = WindowInsets.navigationBars.asPaddingValues(),
+    ) {
+        if (extension.isObsolete) {
+            item {
+                WarningBanner(R.string.obsolete_extension_message)
+            }
+        }
+
+        if (extension.isUnofficial) {
+            item {
+                WarningBanner(R.string.unofficial_extension_message)
+            }
+        }
+
+        item {
+            DetailsHeader(extension, onClickUninstall, onClickAppInfo)
+        }
+
+        items(
+            items = sources,
+            key = { it.source.id },
+        ) { source ->
+            SourceSwitchPreference(
+                modifier = Modifier.animateItemPlacement(),
+                source = source,
+                onClickSourcePreferences = onClickSourcePreferences,
+                onClickSource = onClickSource,
+            )
+        }
+    }
+}
+
+@Composable
+private fun WarningBanner(@StringRes textRes: Int) {
+    Box(
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(MaterialTheme.colorScheme.error)
+            .padding(16.dp),
+        contentAlignment = Alignment.Center,
+    ) {
+        Text(
+            text = stringResource(textRes),
+            color = MaterialTheme.colorScheme.onError,
+        )
+    }
+}
+
+@Composable
+private fun DetailsHeader(
+    extension: Extension,
+    onClickUninstall: () -> Unit,
+    onClickAppInfo: () -> Unit,
+) {
+    val context = LocalContext.current
+
+    Column {
+        Row(
+            modifier = Modifier.padding(
+                start = horizontalPadding,
+                end = horizontalPadding,
+                top = 16.dp,
+                bottom = 8.dp,
+            ),
+        ) {
+            ExtensionIcon(
+                modifier = Modifier
+                    .height(56.dp)
+                    .width(56.dp),
+                extension = extension,
+            )
+
+            Column(
+                modifier = Modifier.padding(start = 16.dp),
+            ) {
+                Text(
+                    text = extension.name,
+                    style = MaterialTheme.typography.titleMedium,
+                )
+                Text(
+                    text = stringResource(R.string.ext_version_info, extension.versionName),
+                    style = MaterialTheme.typography.bodySmall,
+                )
+                Text(
+                    text = stringResource(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context)),
+                    style = MaterialTheme.typography.bodySmall,
+                )
+                if (extension.isNsfw) {
+                    Text(
+                        text = stringResource(R.string.ext_nsfw_warning),
+                        color = MaterialTheme.colorScheme.error,
+                        style = MaterialTheme.typography.bodySmall,
+                    )
+                }
+                Text(
+                    text = extension.pkgName,
+                    style = MaterialTheme.typography.bodySmall,
+                )
+            }
+        }
+
+        Row(
+            modifier = Modifier.padding(
+                start = horizontalPadding,
+                end = horizontalPadding,
+                top = 8.dp,
+                bottom = 16.dp,
+            ),
+        ) {
+            OutlinedButton(
+                modifier = Modifier.weight(1f),
+                onClick = onClickUninstall,
+            ) {
+                Text(stringResource(R.string.ext_uninstall))
+            }
+
+            Spacer(Modifier.width(16.dp))
+
+            Button(
+                modifier = Modifier.weight(1f),
+                onClick = onClickAppInfo,
+            ) {
+                Text(
+                    text = stringResource(R.string.ext_app_info),
+                    color = MaterialTheme.colorScheme.onPrimary,
+                )
+            }
+        }
+
+        Divider()
+    }
+}
+
+@Composable
+private fun SourceSwitchPreference(
+    modifier: Modifier = Modifier,
+    source: ExtensionSourceItem,
+    onClickSourcePreferences: (sourceId: Long) -> Unit,
+    onClickSource: (sourceId: Long) -> Unit,
+) {
+    val context = LocalContext.current
+
+    PreferenceRow(
+        modifier = modifier,
+        title = if (source.labelAsName) {
+            source.source.toString()
+        } else {
+            LocaleHelper.getSourceDisplayName(source.source.lang, context)
+        },
+        onClick = { onClickSource(source.source.id) },
+        action = {
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                if (source.source is ConfigurableSource) {
+                    IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
+                        Icon(
+                            imageVector = Icons.Outlined.Settings,
+                            contentDescription = stringResource(R.string.label_settings),
+                            tint = MaterialTheme.colorScheme.onSurface,
+                        )
+                    }
+                }
+
+                Switch(checked = source.enabled, onCheckedChange = null)
+            }
+        },
+    )
+}

+ 2 - 2
app/src/main/java/eu/kanade/presentation/source/MigrateMangaScreen.kt → app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.source
+package eu.kanade.presentation.browse
 
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
@@ -51,7 +51,7 @@ fun MigrateMangaContent(
     onClickCover: (Manga) -> Unit,
 ) {
     if (list.isEmpty()) {
-        EmptyScreen(textResource = R.string.migrate_empty_screen)
+        EmptyScreen(textResource = R.string.empty_screen)
         return
     }
     LazyColumn(

+ 2 - 2
app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt → app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.source
+package eu.kanade.presentation.browse
 
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
@@ -17,10 +17,10 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import eu.kanade.domain.source.model.Source
+import eu.kanade.presentation.browse.components.BaseSourceItem
 import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.ItemBadges
 import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.source.components.BaseSourceItem
 import eu.kanade.presentation.theme.header
 import eu.kanade.presentation.util.horizontalPadding
 import eu.kanade.presentation.util.plus

+ 3 - 2
app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt → app/src/main/java/eu/kanade/presentation/browse/SourceFilterScreen.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.source
+package eu.kanade.presentation.browse
 
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
@@ -16,10 +16,10 @@ 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.browse.components.BaseSourceItem
 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
@@ -59,6 +59,7 @@ fun SourceFilterContent(
         EmptyScreen(textResource = R.string.source_filter_empty_screen)
         return
     }
+
     LazyColumn(
         modifier = Modifier.nestedScroll(nestedScrollInterop),
         contentPadding = WindowInsets.navigationBars.asPaddingValues(),

+ 2 - 2
app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt → app/src/main/java/eu/kanade/presentation/browse/SourceScreen.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.source
+package eu.kanade.presentation.browse
 
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Column
@@ -32,9 +32,9 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import eu.kanade.domain.source.model.Pin
 import eu.kanade.domain.source.model.Source
+import eu.kanade.presentation.browse.components.BaseSourceItem
 import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.source.components.BaseSourceItem
 import eu.kanade.presentation.theme.header
 import eu.kanade.presentation.util.horizontalPadding
 import eu.kanade.presentation.util.plus

+ 1 - 3
app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt → app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt

@@ -1,4 +1,4 @@
-package eu.kanade.presentation.source.components
+package eu.kanade.presentation.browse.components
 
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.RowScope
@@ -9,8 +9,6 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow
 import eu.kanade.domain.source.model.Source
-import eu.kanade.presentation.browse.components.BaseBrowseItem
-import eu.kanade.presentation.browse.components.SourceIcon
 import eu.kanade.presentation.util.horizontalPadding
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 

+ 17 - 164
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt

@@ -1,59 +1,30 @@
 package eu.kanade.tachiyomi.ui.browse.extension.details
 
 import android.annotation.SuppressLint
-import android.content.Context
 import android.os.Bundle
-import android.util.TypedValue
-import android.view.LayoutInflater
 import android.view.Menu
 import android.view.MenuInflater
 import android.view.MenuItem
-import android.view.View
-import androidx.appcompat.view.ContextThemeWrapper
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.core.os.bundleOf
-import androidx.preference.PreferenceGroupAdapter
-import androidx.preference.PreferenceManager
-import androidx.preference.PreferenceScreen
-import androidx.preference.SwitchPreferenceCompat
-import androidx.recyclerview.widget.ConcatAdapter
-import androidx.recyclerview.widget.LinearLayoutManager
-import dev.chrisbanes.insetter.applyInsetter
+import eu.kanade.presentation.browse.ExtensionDetailsScreen
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
-import eu.kanade.tachiyomi.extension.model.Extension
 import eu.kanade.tachiyomi.network.NetworkHelper
-import eu.kanade.tachiyomi.source.CatalogueSource
-import eu.kanade.tachiyomi.source.ConfigurableSource
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.getPreferenceKey
 import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.ComposeController
 import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
 import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.util.preference.DSL
-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.switchPreference
-import eu.kanade.tachiyomi.util.preference.switchSettingsPreference
-import eu.kanade.tachiyomi.util.system.LocaleHelper
 import eu.kanade.tachiyomi.util.system.logcat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
 import okhttp3.HttpUrl.Companion.toHttpUrl
 import uy.kohesive.injekt.injectLazy
 
 @SuppressLint("RestrictedApi")
 class ExtensionDetailsController(bundle: Bundle? = null) :
-    NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle) {
+    ComposeController<ExtensionDetailsPresenter>(bundle) {
 
-    private val preferences: PreferencesHelper by injectLazy()
     private val network: NetworkHelper by injectLazy()
 
-    private var preferenceScreen: PreferenceScreen? = null
-
     constructor(pkgName: String) : this(
         bundleOf(PKGNAME_KEY to pkgName),
     )
@@ -62,122 +33,22 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
         setHasOptionsMenu(true)
     }
 
-    override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding {
-        val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
-        return ExtensionDetailControllerBinding.inflate(themedInflater)
-    }
-
-    override fun createPresenter(): ExtensionDetailsPresenter {
-        return ExtensionDetailsPresenter(this, args.getString(PKGNAME_KEY)!!)
-    }
-
-    override fun getTitle(): String? {
-        return resources?.getString(R.string.label_extension_info)
-    }
-
-    @SuppressLint("PrivateResource")
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        binding.extensionPrefsRecycler.applyInsetter {
-            type(navigationBars = true) {
-                padding()
-            }
-        }
+    override fun getTitle() = resources?.getString(R.string.label_extension_info)
 
-        val extension = presenter.extension ?: return
-        val context = view.context
+    override fun createPresenter() = ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
 
-        binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context)
-        binding.extensionPrefsRecycler.adapter = ConcatAdapter(
-            ExtensionDetailsHeaderAdapter(presenter),
-            initPreferencesAdapter(context, extension),
+    @Composable
+    override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
+        ExtensionDetailsScreen(
+            nestedScrollInterop = nestedScrollInterop,
+            presenter = presenter,
+            onClickUninstall = { presenter.uninstallExtension() },
+            onClickAppInfo = { presenter.openInSettings() },
+            onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
+            onClickSource = { presenter.toggleSource(it) },
         )
     }
 
-    private fun initPreferencesAdapter(context: Context, extension: Extension.Installed): PreferenceGroupAdapter {
-        val themedContext = getPreferenceThemeContext()
-        val manager = PreferenceManager(themedContext)
-        manager.preferenceDataStore = EmptyPreferenceDataStore()
-        val screen = manager.createPreferenceScreen(themedContext)
-        preferenceScreen = screen
-
-        val isMultiSource = extension.sources.size > 1
-        val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
-
-        with(screen) {
-            if (isMultiSource && isMultiLangSingleSource.not()) {
-                multiLanguagePreference(context, extension.sources)
-            } else {
-                singleLanguagePreference(context, extension.sources)
-            }
-        }
-
-        return PreferenceGroupAdapter(screen)
-    }
-
-    private fun PreferenceScreen.singleLanguagePreference(context: Context, sources: List<Source>) {
-        sources
-            .map { source -> LocaleHelper.getSourceDisplayName(source.lang, context) to source }
-            .sortedWith(compareBy({ (_, source) -> !source.isEnabled() }, { (lang, _) -> lang.lowercase() }))
-            .forEach { (lang, source) ->
-                sourceSwitchPreference(source, lang)
-            }
-    }
-
-    private fun PreferenceScreen.multiLanguagePreference(context: Context, sources: List<Source>) {
-        sources
-            .groupBy { (it as CatalogueSource).lang }
-            .toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
-            .forEach { entry ->
-                entry.value
-                    .sortedWith(compareBy({ source -> !source.isEnabled() }, { source -> source.name.lowercase() }))
-                    .forEach { source ->
-                        sourceSwitchPreference(source, source.toString())
-                    }
-            }
-    }
-
-    private fun PreferenceScreen.sourceSwitchPreference(source: Source, name: String) {
-        val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
-            key = source.getPreferenceKey()
-            title = name
-            isPersistent = false
-            isChecked = source.isEnabled()
-
-            onChange { newValue ->
-                val checked = newValue as Boolean
-                toggleSource(source, checked)
-                true
-            }
-
-            // React to enable/disable all changes
-            preferences.disabledSources().asFlow()
-                .onEach {
-                    val enabled = source.isEnabled()
-                    isChecked = enabled
-                }
-                .launchIn(viewScope)
-        }
-
-        // Source enable/disable
-        if (source is ConfigurableSource) {
-            switchSettingsPreference {
-                block()
-                onSettingsClick = View.OnClickListener {
-                    router.pushController(SourcePreferencesController(source.id))
-                }
-            }
-        } else {
-            switchPreference(block)
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        preferenceScreen = null
-        super.onDestroyView(view)
-    }
-
     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
         inflater.inflate(R.menu.extension_details, menu)
 
@@ -203,15 +74,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
     }
 
     private fun toggleAllSources(enable: Boolean) {
-        presenter.extension?.sources?.forEach { toggleSource(it, enable) }
-    }
-
-    private fun toggleSource(source: Source, enable: Boolean) {
-        if (enable) {
-            preferences.disabledSources() -= source.id.toString()
-        } else {
-            preferences.disabledSources() += source.id.toString()
-        }
+        presenter.toggleSources(enable)
     }
 
     private fun openChangelog() {
@@ -263,16 +126,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
 
         logcat { "Cleared $cleared cookies for: ${urls.joinToString()}" }
     }
-
-    private fun Source.isEnabled(): Boolean {
-        return id.toString() !in preferences.disabledSources().get()
-    }
-
-    private fun getPreferenceThemeContext(): Context {
-        val tv = TypedValue()
-        activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
-        return ContextThemeWrapper(activity, tv.resourceId)
-    }
 }
 
 private const val PKGNAME_KEY = "pkg_name"

+ 0 - 62
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt

@@ -1,62 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.extension.details
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
-import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
-import eu.kanade.tachiyomi.util.system.LocaleHelper
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import reactivecircus.flowbinding.android.view.clicks
-
-class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPresenter) :
-    RecyclerView.Adapter<ExtensionDetailsHeaderAdapter.HeaderViewHolder>() {
-
-    private lateinit var binding: ExtensionDetailHeaderBinding
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
-        binding = ExtensionDetailHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
-        return HeaderViewHolder(binding.root)
-    }
-
-    override fun getItemCount(): Int = 1
-
-    override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
-        holder.bind()
-    }
-
-    inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
-        fun bind() {
-            val extension = presenter.extension ?: return
-            val context = view.context
-
-            extension.getApplicationIcon(context)?.let { binding.icon.setImageDrawable(it) }
-            binding.title.text = extension.name
-            binding.version.text = context.getString(R.string.ext_version_info, extension.versionName)
-            binding.lang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
-            binding.nsfw.isVisible = extension.isNsfw
-            binding.pkgname.text = extension.pkgName
-
-            binding.btnUninstall.clicks()
-                .onEach { presenter.uninstallExtension() }
-                .launchIn(presenter.presenterScope)
-            binding.btnAppInfo.clicks()
-                .onEach { presenter.openInSettings() }
-                .launchIn(presenter.presenterScope)
-
-            if (extension.isObsolete) {
-                binding.warningBanner.isVisible = true
-                binding.warningBanner.setText(R.string.obsolete_extension_message)
-            }
-
-            if (extension.isUnofficial) {
-                binding.warningBanner.isVisible = true
-                binding.warningBanner.setText(R.string.unofficial_extension_message)
-            }
-        }
-    }
-}

+ 50 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt

@@ -1,27 +1,58 @@
 package eu.kanade.tachiyomi.ui.browse.extension.details
 
+import android.app.Application
 import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
 import android.provider.Settings
+import eu.kanade.domain.extension.interactor.GetExtensionSources
+import eu.kanade.domain.source.interactor.ToggleSource
 import eu.kanade.tachiyomi.extension.ExtensionManager
+import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.lang.launchIO
+import eu.kanade.tachiyomi.util.system.LocaleHelper
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
 import rx.android.schedulers.AndroidSchedulers
-import uy.kohesive.injekt.injectLazy
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 
 class ExtensionDetailsPresenter(
-    private val controller: ExtensionDetailsController,
     private val pkgName: String,
+    private val context: Application = Injekt.get(),
+    private val getExtensionSources: GetExtensionSources = Injekt.get(),
+    private val toggleSource: ToggleSource = Injekt.get(),
+    private val extensionManager: ExtensionManager = Injekt.get(),
 ) : BasePresenter<ExtensionDetailsController>() {
 
-    private val extensionManager: ExtensionManager by injectLazy()
-
     val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
 
+    private val _state: MutableStateFlow<List<ExtensionSourceItem>> = MutableStateFlow(emptyList())
+    val sourcesState: StateFlow<List<ExtensionSourceItem>> = _state.asStateFlow()
+
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
 
+        val extension = extension ?: return
+
         bindToUninstalledExtension()
+
+        presenterScope.launchIO {
+            getExtensionSources.subscribe(extension)
+                .map {
+                    it.sortedWith(
+                        compareBy(
+                            { item -> item.enabled.not() },
+                            { item -> if (item.labelAsName) item.source.name else LocaleHelper.getSourceDisplayName(item.source.lang, context).lowercase() },
+                        ),
+                    )
+                }
+                .collectLatest { _state.value = it }
+        }
     }
 
     private fun bindToUninstalledExtension() {
@@ -45,6 +76,20 @@ class ExtensionDetailsPresenter(
         val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
             data = Uri.fromParts("package", pkgName, null)
         }
-        controller.startActivity(intent)
+        view?.startActivity(intent)
+    }
+
+    fun toggleSource(sourceId: Long) {
+        toggleSource.await(sourceId)
+    }
+
+    fun toggleSources(enable: Boolean) {
+        extension?.sources?.forEach { toggleSource.await(it.id, enable) }
     }
 }
+
+data class ExtensionSourceItem(
+    val source: Source,
+    val enabled: Boolean,
+    val labelAsName: Boolean,
+)

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt

@@ -4,7 +4,7 @@ import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.core.os.bundleOf
-import eu.kanade.presentation.source.MigrateMangaScreen
+import eu.kanade.presentation.browse.MigrateMangaScreen
 import eu.kanade.tachiyomi.ui.base.controller.ComposeController
 import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt

@@ -5,7 +5,7 @@ import android.view.MenuInflater
 import android.view.MenuItem
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import eu.kanade.presentation.source.MigrateSourceScreen
+import eu.kanade.presentation.browse.MigrateSourceScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.base.controller.ComposeController
 import eu.kanade.tachiyomi.ui.base.controller.pushController

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

@@ -9,7 +9,7 @@ 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.presentation.browse.SourceScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController

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

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.source
 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.presentation.browse.SourceFilterScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.base.controller.ComposeController
 

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

@@ -28,6 +28,7 @@ class SourceFilterPresenter(
 
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
+
         presenterScope.launchIO {
             getLanguagesWithSources.subscribe()
                 .catch { exception ->

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

@@ -6,7 +6,7 @@ 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
-import eu.kanade.presentation.source.SourceUiModel
+import eu.kanade.presentation.browse.SourceUiModel
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.util.lang.launchIO
 import kotlinx.coroutines.flow.MutableStateFlow

+ 0 - 5
app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceDSL.kt

@@ -23,7 +23,6 @@ import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.widget.preference.AdaptiveTitlePreferenceCategory
 import eu.kanade.tachiyomi.widget.preference.IntListPreference
 import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
-import eu.kanade.tachiyomi.widget.preference.SwitchSettingsPreference
 
 @DslMarker
 @Target(AnnotationTarget.TYPE)
@@ -56,10 +55,6 @@ inline fun PreferenceGroup.switchPreferenceCategory(block: (@DSL SwitchPreferenc
     return initThenAdd(SwitchPreferenceCategory(context), block)
 }
 
-inline fun PreferenceGroup.switchSettingsPreference(block: (@DSL SwitchSettingsPreference).() -> Unit): SwitchSettingsPreference {
-    return initThenAdd(SwitchSettingsPreference(context), block)
-}
-
 inline fun PreferenceGroup.checkBoxPreference(block: (@DSL CheckBoxPreference).() -> Unit): CheckBoxPreference {
     return initThenAdd(CheckBoxPreference(context), block)
 }

+ 0 - 34
app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchSettingsPreference.kt

@@ -1,34 +0,0 @@
-package eu.kanade.tachiyomi.widget.preference
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.util.AttributeSet
-import android.view.MotionEvent
-import android.view.View
-import androidx.preference.PreferenceViewHolder
-import androidx.preference.SwitchPreferenceCompat
-import eu.kanade.tachiyomi.R
-
-class SwitchSettingsPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    SwitchPreferenceCompat(context, attrs) {
-
-    var onSettingsClick: View.OnClickListener? = null
-
-    init {
-        widgetLayoutResource = R.layout.pref_settings
-    }
-
-    @SuppressLint("ClickableViewAccessibility")
-    override fun onBindViewHolder(holder: PreferenceViewHolder) {
-        super.onBindViewHolder(holder)
-
-        holder.findViewById(R.id.button).setOnClickListener {
-            onSettingsClick?.onClick(it)
-        }
-
-        // Disable swiping to align with SwitchPreferenceCompat
-        holder.findViewById(R.id.switchWidget).setOnTouchListener { _, event ->
-            event.actionMasked == MotionEvent.ACTION_MOVE
-        }
-    }
-}

+ 0 - 5
app/src/main/res/layout/extension_detail_controller.xml

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/extension_prefs_recycler"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content" />

+ 0 - 130
app/src/main/res/layout/extension_detail_header.xml

@@ -1,130 +0,0 @@
-<?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">
-
-    <TextView
-        android:id="@+id/warning_banner"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="?attr/colorError"
-        android:gravity="center"
-        android:padding="16dp"
-        android:textColor="?attr/colorOnError"
-        android:visibility="gone"
-        tools:visibility="visible" />
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:padding="16dp">
-
-        <ImageView
-            android:id="@+id/icon"
-            android:layout_width="56dp"
-            android:layout_height="56dp"
-            android:src="@mipmap/ic_launcher"
-            app:layout_constraintBottom_toBottomOf="@id/pkgname"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            tools:ignore="ContentDescription" />
-
-        <TextView
-            android:id="@+id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="16dp"
-            android:elevation="3dp"
-            android:textAppearance="?attr/textAppearanceTitleMedium"
-            app:layout_constraintStart_toEndOf="@id/icon"
-            app:layout_constraintTop_toTopOf="parent"
-            tools:text="Tachiyomi: Extension" />
-
-        <TextView
-            android:id="@+id/version"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:layout_weight="1"
-            android:elevation="3dp"
-            android:gravity="center"
-            android:textAppearance="?attr/textAppearanceBodySmall"
-            app:layout_constraintStart_toStartOf="@id/title"
-            app:layout_constraintTop_toBottomOf="@id/title"
-            tools:text="Version: 1.0.0" />
-
-        <TextView
-            android:id="@+id/lang"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:layout_weight="1"
-            android:elevation="3dp"
-            android:gravity="center"
-            android:textAppearance="?attr/textAppearanceBodySmall"
-            app:layout_constraintStart_toStartOf="@id/title"
-            app:layout_constraintTop_toBottomOf="@id/version"
-            tools:text="Language: English" />
-
-        <TextView
-            android:id="@+id/nsfw"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:layout_weight="1"
-            android:elevation="3dp"
-            android:gravity="center"
-            android:text="@string/ext_nsfw_warning"
-            android:textAppearance="?attr/textAppearanceBodySmall"
-            android:textColor="?attr/colorError"
-            android:visibility="gone"
-            app:layout_constraintStart_toStartOf="@id/title"
-            app:layout_constraintTop_toBottomOf="@id/lang"
-            tools:visibility="visible" />
-
-        <TextView
-            android:id="@+id/pkgname"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:elevation="3dp"
-            android:ellipsize="middle"
-            android:singleLine="true"
-            android:textAppearance="?attr/textAppearanceBodySmall"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="@id/title"
-            app:layout_constraintTop_toBottomOf="@id/nsfw"
-            tools:text="eu.kanade.tachiyomi.extension.en.myext" />
-
-        <Button
-            android:id="@+id/btn_uninstall"
-            style="@style/Widget.Tachiyomi.Button.OutlinedButton"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="8dp"
-            android:layout_marginEnd="4dp"
-            android:text="@string/ext_uninstall"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toStartOf="@+id/btn_app_info"
-            app:layout_constraintTop_toBottomOf="@id/pkgname" />
-
-        <Button
-            android:id="@+id/btn_app_info"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="8dp"
-            android:layout_marginStart="4dp"
-            android:text="@string/ext_app_info"
-            app:layout_constraintStart_toEndOf="@+id/btn_uninstall"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/pkgname" />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-    <com.google.android.material.divider.MaterialDivider
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-</LinearLayout>

+ 0 - 25
app/src/main/res/layout/pref_settings.xml

@@ -1,25 +0,0 @@
-<?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"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:gravity="center_vertical">
-
-    <ImageButton
-        android:id="@+id/button"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="8dp"
-        android:background="?attr/selectableItemBackgroundBorderless"
-        android:contentDescription="@string/label_settings"
-        android:padding="8dp"
-        app:srcCompat="@drawable/ic_settings_24dp"
-        app:tint="?attr/colorOnBackground" />
-
-    <!-- Matches ID used in SwitchPreferenceCompat -->
-    <androidx.appcompat.widget.SwitchCompat
-        android:id="@+id/switchWidget"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content" />
-
-</LinearLayout>

+ 1 - 1
app/src/main/res/values/strings.xml

@@ -730,7 +730,7 @@
     <string name="migration_selection_prompt">Select a source to migrate from</string>
     <string name="migrate">Migrate</string>
     <string name="copy">Copy</string>
-    <string name="migrate_empty_screen">Well, this is awkward</string>
+    <string name="empty_screen">Well, this is awkward</string>
 
     <!-- Downloads activity and service -->
     <string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>