From 5b189a909b06156c5bbbb7b8d7570ee27c16bdf6 Mon Sep 17 00:00:00 2001
From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
Date: Sat, 3 Dec 2022 01:14:18 +0700
Subject: [PATCH] Use Voyager on Source Preference screen (#8651)

---
 .../ui/base/delegate/ThemingDelegate.kt       |   3 +
 .../details/ExtensionDetailsScreen.kt         |   5 +-
 .../details/SourcePreferencesController.kt    | 179 ------------------
 .../details/SourcePreferencesPresenter.kt     |  14 --
 .../details/SourcePreferencesScreen.kt        | 174 +++++++++++++++++
 app/src/main/res/values/styles.xml            |   3 +
 app/src/main/res/values/themes.xml            |   1 +
 7 files changed, 182 insertions(+), 197 deletions(-)
 delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesController.kt
 delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesPresenter.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt

diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt
index 4ad7474fe..aa9408be3 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/ThemingDelegate.kt
@@ -53,6 +53,9 @@ interface ThemingDelegate {
                 resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
             }
 
+            // For source preference theme
+            resIds += R.style.PreferenceThemeOverlay_Tachiyomi
+
             return resIds
         }
     }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt
index e1e82b1d6..402fc232c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt
@@ -12,8 +12,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.browse.ExtensionDetailsScreen
 import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.util.LocalRouter
-import eu.kanade.tachiyomi.ui.base.controller.pushController
 import kotlinx.coroutines.flow.collectLatest
 
 data class ExtensionDetailsScreen(
@@ -32,13 +30,12 @@ data class ExtensionDetailsScreen(
         }
 
         val navigator = LocalNavigator.currentOrThrow
-        val router = LocalRouter.currentOrThrow
         val uriHandler = LocalUriHandler.current
 
         ExtensionDetailsScreen(
             navigateUp = navigator::pop,
             state = state,
-            onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
+            onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
             onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
             onClickReadme = { uriHandler.openUri(screenModel.getReadmeUrl()) },
             onClickEnableAll = { screenModel.toggleSources(true) },
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesController.kt
deleted file mode 100644
index 0ceb124ca..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesController.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-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.View
-import androidx.appcompat.view.ContextThemeWrapper
-import androidx.core.os.bundleOf
-import androidx.preference.DialogPreference
-import androidx.preference.EditTextPreference
-import androidx.preference.EditTextPreferenceDialogController
-import androidx.preference.ListPreference
-import androidx.preference.ListPreferenceDialogController
-import androidx.preference.MultiSelectListPreference
-import androidx.preference.MultiSelectListPreferenceDialogController
-import androidx.preference.Preference
-import androidx.preference.PreferenceGroupAdapter
-import androidx.preference.PreferenceManager
-import androidx.preference.PreferenceScreen
-import androidx.preference.get
-import androidx.preference.getOnBindEditTextListener
-import androidx.preference.isNotEmpty
-import androidx.recyclerview.widget.LinearLayoutManager
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
-import eu.kanade.tachiyomi.databinding.SourcePreferencesControllerBinding
-import eu.kanade.tachiyomi.source.ConfigurableSource
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.getPreferenceKey
-import eu.kanade.tachiyomi.ui.base.controller.NucleusController
-import eu.kanade.tachiyomi.util.system.logcat
-import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
-import logcat.LogPriority
-
-@SuppressLint("RestrictedApi")
-class SourcePreferencesController(bundle: Bundle? = null) :
-    NucleusController<SourcePreferencesControllerBinding, SourcePreferencesPresenter>(bundle),
-    PreferenceManager.OnDisplayPreferenceDialogListener,
-    DialogPreference.TargetFragment {
-
-    private var lastOpenPreferencePosition: Int? = null
-
-    private var preferenceScreen: PreferenceScreen? = null
-
-    constructor(sourceId: Long) : this(
-        bundleOf(SOURCE_ID to sourceId),
-    )
-
-    override fun createBinding(inflater: LayoutInflater): SourcePreferencesControllerBinding {
-        val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
-        return SourcePreferencesControllerBinding.inflate(themedInflater)
-    }
-
-    override fun createPresenter(): SourcePreferencesPresenter {
-        return SourcePreferencesPresenter(args.getLong(SOURCE_ID))
-    }
-
-    override fun getTitle(): String? {
-        return presenter.source?.toString()
-    }
-
-    @SuppressLint("PrivateResource")
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        val source = presenter.source ?: return
-        val context = view.context
-
-        val themedContext by lazy { getPreferenceThemeContext() }
-        val manager = PreferenceManager(themedContext)
-        val dataStore = SharedPreferencesDataStore(
-            context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE),
-        )
-        manager.preferenceDataStore = dataStore
-        manager.onDisplayPreferenceDialogListener = this
-        val screen = manager.createPreferenceScreen(themedContext)
-        preferenceScreen = screen
-
-        try {
-            addPreferencesForSource(screen, source)
-        } catch (e: AbstractMethodError) {
-            logcat(LogPriority.ERROR) { "Source did not implement [addPreferencesForSource]: ${source.name}" }
-        }
-
-        manager.setPreferences(screen)
-
-        binding.recycler.layoutManager = LinearLayoutManager(context)
-        binding.recycler.adapter = PreferenceGroupAdapter(screen)
-    }
-
-    override fun onDestroyView(view: View) {
-        preferenceScreen = null
-        super.onDestroyView(view)
-    }
-
-    override fun onSaveInstanceState(outState: Bundle) {
-        lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) }
-        super.onSaveInstanceState(outState)
-    }
-
-    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
-        super.onRestoreInstanceState(savedInstanceState)
-        lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int
-    }
-
-    private fun addPreferencesForSource(screen: PreferenceScreen, source: Source) {
-        val context = screen.context
-
-        if (source is ConfigurableSource) {
-            val newScreen = screen.preferenceManager.createPreferenceScreen(context)
-            source.setupPreferenceScreen(newScreen)
-
-            // Reparent the preferences
-            while (newScreen.isNotEmpty()) {
-                val pref = newScreen[0]
-                pref.isIconSpaceReserved = false
-                pref.order = Int.MAX_VALUE // reset to default order
-
-                // Apply incognito IME for EditTextPreference
-                if (pref is EditTextPreference) {
-                    val setListener = pref.getOnBindEditTextListener()
-                    pref.setOnBindEditTextListener {
-                        setListener?.onBindEditText(it)
-                        it.setIncognito(viewScope)
-                    }
-                }
-
-                newScreen.removePreference(pref)
-                screen.addPreference(pref)
-            }
-        }
-    }
-
-    private fun getPreferenceThemeContext(): Context {
-        val tv = TypedValue()
-        activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
-        return ContextThemeWrapper(activity, tv.resourceId)
-    }
-
-    override fun onDisplayPreferenceDialog(preference: Preference) {
-        if (!isAttached) return
-
-        val screen = preference.parent!!
-
-        lastOpenPreferencePosition = (0 until screen.preferenceCount).indexOfFirst {
-            screen[it] === preference
-        }
-
-        val f = when (preference) {
-            is EditTextPreference ->
-                EditTextPreferenceDialogController
-                    .newInstance(preference.getKey())
-            is ListPreference ->
-                ListPreferenceDialogController
-                    .newInstance(preference.getKey())
-            is MultiSelectListPreference ->
-                MultiSelectListPreferenceDialogController
-                    .newInstance(preference.getKey())
-            else -> throw IllegalArgumentException(
-                "Tried to display dialog for unknown " +
-                    "preference type. Did you forget to override onDisplayPreferenceDialog()?",
-            )
-        }
-        f.targetController = this
-        f.showDialog(router)
-    }
-
-    @Suppress("UNCHECKED_CAST")
-    override fun <T : Preference> findPreference(key: CharSequence): T? {
-        // We track [lastOpenPreferencePosition] when displaying the dialog
-        // [key] isn't useful since there may be duplicates
-        return preferenceScreen!![lastOpenPreferencePosition!!] as T
-    }
-}
-
-private const val SOURCE_ID = "source_id"
-private const val LASTOPENPREFERENCE_KEY = "last_open_preference"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesPresenter.kt
deleted file mode 100644
index 1ac5af766..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesPresenter.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.extension.details
-
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-
-class SourcePreferencesPresenter(
-    val sourceId: Long,
-    sourceManager: SourceManager = Injekt.get(),
-) : BasePresenter<SourcePreferencesController>() {
-
-    val source = sourceManager.get(sourceId)
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt
new file mode 100644
index 000000000..bc36d15d7
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt
@@ -0,0 +1,174 @@
+package eu.kanade.tachiyomi.ui.browse.extension.details
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.os.bundleOf
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.fragment.app.commit
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.DialogPreference
+import androidx.preference.EditTextPreference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceScreen
+import androidx.preference.forEach
+import androidx.preference.getOnBindEditTextListener
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import eu.kanade.presentation.components.Scaffold
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
+import eu.kanade.tachiyomi.source.ConfigurableSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.getPreferenceKey
+import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SourcePreferencesScreen(val sourceId: Long) : Screen {
+
+    override val key = uniqueScreenKey
+
+    @Composable
+    override fun Content() {
+        val context = LocalContext.current
+        val navigator = LocalNavigator.currentOrThrow
+
+        Scaffold(
+            topBar = {
+                TopAppBar(
+                    title = { Text(text = Injekt.get<SourceManager>().get(sourceId)!!.toString()) },
+                    navigationIcon = {
+                        IconButton(onClick = navigator::pop) {
+                            Icon(
+                                imageVector = Icons.Outlined.ArrowBack,
+                                contentDescription = stringResource(R.string.abc_action_bar_up_description),
+                            )
+                        }
+                    },
+                    scrollBehavior = it,
+                )
+            },
+        ) { contentPadding ->
+            FragmentContainer(
+                fragmentManager = (context as FragmentActivity).supportFragmentManager,
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(contentPadding),
+            ) {
+                val fragment = SourcePreferencesFragment.getInstance(sourceId)
+                add(it, fragment, null)
+            }
+        }
+    }
+
+    /**
+     * From https://stackoverflow.com/questions/60520145/fragment-container-in-jetpack-compose/70817794#70817794
+     */
+    @Composable
+    private fun FragmentContainer(
+        fragmentManager: FragmentManager,
+        modifier: Modifier = Modifier,
+        commit: FragmentTransaction.(containerId: Int) -> Unit,
+    ) {
+        val containerId by rememberSaveable {
+            mutableStateOf(View.generateViewId())
+        }
+        var initialized by rememberSaveable { mutableStateOf(false) }
+        AndroidView(
+            modifier = modifier,
+            factory = { context ->
+                FragmentContainerView(context)
+                    .apply { id = containerId }
+            },
+            update = { view ->
+                if (!initialized) {
+                    fragmentManager.commit { commit(view.id) }
+                    initialized = true
+                } else {
+                    fragmentManager.onContainerAvailable(view)
+                }
+            },
+        )
+    }
+
+    /** Access to package-private method in FragmentManager through reflection */
+    private fun FragmentManager.onContainerAvailable(view: FragmentContainerView) {
+        val method = FragmentManager::class.java.getDeclaredMethod(
+            "onContainerAvailable",
+            FragmentContainerView::class.java,
+        )
+        method.isAccessible = true
+        method.invoke(this, view)
+    }
+}
+
+class SourcePreferencesFragment : PreferenceFragmentCompat() {
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        preferenceScreen = populateScreen()
+    }
+
+    private fun populateScreen(): PreferenceScreen {
+        val sourceId = requireArguments().getLong(SOURCE_ID)
+        val source = Injekt.get<SourceManager>().get(sourceId)!!
+
+        check(source is ConfigurableSource)
+
+        val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
+        val dataStore = SharedPreferencesDataStore(sharedPreferences)
+        preferenceManager.preferenceDataStore = dataStore
+
+        val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
+        source.setupPreferenceScreen(sourceScreen)
+        sourceScreen.forEach { pref ->
+            pref.isIconSpaceReserved = false
+            if (pref is DialogPreference) {
+                pref.dialogTitle = pref.title
+            }
+
+            // Apply incognito IME for EditTextPreference
+            if (pref is EditTextPreference) {
+                val setListener = pref.getOnBindEditTextListener()
+                pref.setOnBindEditTextListener {
+                    setListener?.onBindEditText(it)
+                    it.setIncognito(lifecycleScope)
+                }
+            }
+        }
+
+        return sourceScreen
+    }
+
+    companion object {
+        private const val SOURCE_ID = "source_id"
+
+        fun getInstance(sourceId: Long): SourcePreferencesFragment {
+            val fragment = SourcePreferencesFragment()
+            fragment.arguments = bundleOf(SOURCE_ID to sourceId)
+            return fragment
+        }
+    }
+}
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 50cdb5c96..f3ed9542f 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -20,6 +20,9 @@
     <style name="ThemeOverlay.Tachiyomi.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
         <item name="android:textColorPrimary">?attr/colorOnSurface</item>
         <item name="android:textColor">?attr/colorOnSurface</item>
+        <item name="android:colorBackground">?attr/colorSurface</item>
+        <item name="android:layout">@layout/m3_alert_dialog</item>
+        <item name="dialogCornerRadius">@dimen/m3_alert_dialog_corner_size</item>
     </style>
 
 
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 60d9032ea..a7a60c0b8 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -58,6 +58,7 @@
         <item name="android:enforceStatusBarContrast" tools:targetApi="Q">false</item>
         <item name="android:itemTextAppearance">@style/TextAppearance.Widget.Menu</item>
         <item name="materialAlertDialogTheme">@style/ThemeOverlay.Tachiyomi.MaterialAlertDialog</item>
+        <item name="alertDialogTheme">@style/ThemeOverlay.Tachiyomi.MaterialAlertDialog</item>
         <item name="textAppearanceButton">@style/TextAppearance.Widget.Button</item>
         <item name="android:buttonStyle">?attr/borderlessButtonStyle</item>
         <item name="android:backgroundDimAmount">0.32</item>