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 4ad7474fe3..aa9408be39 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 e1e82b1d6b..402fc232c2 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 0ceb124cad..0000000000 --- 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(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 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 1ac5af7664..0000000000 --- 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() { - - 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 0000000000..bc36d15d7d --- /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().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().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 50cdb5c96a..f3ed9542fa 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -20,6 +20,9 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 60d9032ea8..a7a60c0b85 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -58,6 +58,7 @@ false @style/TextAppearance.Widget.Menu @style/ThemeOverlay.Tachiyomi.MaterialAlertDialog + @style/ThemeOverlay.Tachiyomi.MaterialAlertDialog @style/TextAppearance.Widget.Button ?attr/borderlessButtonStyle 0.32