mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Use Voyager on Source Preference screen (#8651)
This commit is contained in:
		@@ -53,6 +53,9 @@ interface ThemingDelegate {
 | 
			
		||||
                resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // For source preference theme
 | 
			
		||||
            resIds += R.style.PreferenceThemeOverlay_Tachiyomi
 | 
			
		||||
 | 
			
		||||
            return resIds
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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) },
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user