Use Voyager on Source Preference screen (#8651)

This commit is contained in:
Ivan Iskandar 2022-12-03 01:14:18 +07:00 committed by GitHub
parent 75a687138d
commit 5b189a909b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 197 deletions

View file

@ -53,6 +53,9 @@ interface ThemingDelegate {
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
}
// For source preference theme
resIds += R.style.PreferenceThemeOverlay_Tachiyomi
return resIds
}
}

View file

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

View file

@ -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"

View file

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

View file

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

View file

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

View file

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