Move extension preferences to separate controller

This commit is contained in:
arkon 2020-05-13 23:12:35 -04:00
parent 4a8d5098da
commit 9cb45b92e1
5 changed files with 271 additions and 190 deletions

View File

@ -1,51 +1,23 @@
package eu.kanade.tachiyomi.ui.browse.extension
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.view.visible
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
PreferenceManager.OnDisplayPreferenceDialogListener,
DialogPreference.TargetFragment {
private var lastOpenPreferencePosition: Int? = null
private var preferenceScreen: PreferenceScreen? = null
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle) {
constructor(pkgName: String) : this(
Bundle().apply {
@ -54,8 +26,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
)
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
binding = ExtensionDetailControllerBinding.inflate(inflater)
return binding.root
}
@ -93,132 +64,25 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
}
val themedContext by lazy { getPreferenceThemeContext() }
val manager = PreferenceManager(themedContext)
manager.preferenceDataStore = EmptyPreferenceDataStore()
manager.onDisplayPreferenceDialogListener = this
val screen = manager.createPreferenceScreen(themedContext)
preferenceScreen = screen
val multiSource = extension.sources.size > 1
for (source in extension.sources) {
if (source is ConfigurableSource) {
try {
addPreferencesForSource(screen, source, multiSource)
} catch (e: AbstractMethodError) {
Timber.e("Source did not implement [addPreferencesForSource]: ${source.name}")
if (presenter.extension?.sources?.find { it is ConfigurableSource } != null) {
binding.extensionPrefs.visible()
binding.extensionPrefs.clicks()
.onEach { openPreferences() }
.launchIn(scope)
}
}
}
manager.setPreferences(screen)
binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context)
binding.extensionPrefsRecycler.adapter = PreferenceGroupAdapter(screen)
binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
if (screen.preferenceCount == 0) {
binding.extensionPrefsEmptyView.show(R.string.ext_empty_preferences)
}
}
override fun onDestroyView(view: View) {
preferenceScreen = null
super.onDestroyView(view)
}
fun onExtensionUninstalled() {
router.popCurrentController()
}
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, multiSource: Boolean) {
val context = screen.context
// TODO
val dataStore = SharedPreferencesDataStore(/*if (source is HttpSource) {
source.preferences
} else {*/
context.getSharedPreferences("source_${source.id}", Context.MODE_PRIVATE)
/*}*/
private fun openPreferences() {
router.pushController(
ExtensionPreferencesController(presenter.extension!!.pkgName).withFadeTransaction()
)
if (source is ConfigurableSource) {
if (multiSource) {
screen.preferenceCategory {
title = source.toString()
}
}
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
source.setupPreferenceScreen(newScreen)
// Reparent the preferences
while (newScreen.preferenceCount != 0) {
val pref = newScreen.getPreference(0)
pref.isIconSpaceReserved = false
pref.preferenceDataStore = dataStore
pref.order = Int.MAX_VALUE // reset to default order
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.getPreference(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!!.getPreference(lastOpenPreferencePosition!!) as T
}
private companion object {
const val PKGNAME_KEY = "pkg_name"
const val LASTOPENPREFERENCE_KEY = "last_open_preference"
}
}

View File

@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.ui.browse.extension
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
import eu.kanade.tachiyomi.databinding.ExtensionPreferencesControllerBinding
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import timber.log.Timber
@SuppressLint("RestrictedApi")
class ExtensionPreferencesController(bundle: Bundle? = null) :
NucleusController<ExtensionPreferencesControllerBinding, ExtensionPreferencesPresenter>(bundle),
PreferenceManager.OnDisplayPreferenceDialogListener,
DialogPreference.TargetFragment {
private var lastOpenPreferencePosition: Int? = null
private var preferenceScreen: PreferenceScreen? = null
constructor(pkgName: String) : this(
Bundle().apply {
putString(PKGNAME_KEY, pkgName)
}
)
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionPreferencesControllerBinding.inflate(themedInflater)
return binding.root
}
override fun createPresenter(): ExtensionPreferencesPresenter {
return ExtensionPreferencesPresenter(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)
val extension = presenter.extension ?: return
val context = view.context
val themedContext by lazy { getPreferenceThemeContext() }
val manager = PreferenceManager(themedContext)
manager.preferenceDataStore = EmptyPreferenceDataStore()
manager.onDisplayPreferenceDialogListener = this
val screen = manager.createPreferenceScreen(themedContext)
preferenceScreen = screen
val multiSource = extension.sources.size > 1
for (source in extension.sources) {
if (source is ConfigurableSource) {
try {
addPreferencesForSource(screen, source, multiSource)
} catch (e: AbstractMethodError) {
Timber.e("Source did not implement [addPreferencesForSource]: ${source.name}")
}
}
}
manager.setPreferences(screen)
binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context)
binding.extensionPrefsRecycler.adapter = PreferenceGroupAdapter(screen)
binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
if (screen.preferenceCount == 0) {
binding.extensionPrefsEmptyView.show(R.string.ext_empty_preferences)
}
}
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, multiSource: Boolean) {
val context = screen.context
// TODO
val dataStore = SharedPreferencesDataStore(/*if (source is HttpSource) {
source.preferences
} else {*/
context.getSharedPreferences("source_${source.id}", Context.MODE_PRIVATE)
/*}*/
)
if (source is ConfigurableSource) {
if (multiSource) {
screen.preferenceCategory {
title = source.toString()
}
}
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
source.setupPreferenceScreen(newScreen)
// Reparent the preferences
while (newScreen.preferenceCount != 0) {
val pref = newScreen.getPreference(0)
pref.isIconSpaceReserved = false
pref.preferenceDataStore = dataStore
pref.order = Int.MAX_VALUE // reset to default order
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.getPreference(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!!.getPreference(lastOpenPreferencePosition!!) as T
}
private companion object {
const val PKGNAME_KEY = "pkg_name"
const val LASTOPENPREFERENCE_KEY = "last_open_preference"
}
}

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.extension
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionPreferencesPresenter(
val pkgName: String,
extensionManager: ExtensionManager = Injekt.get()
) : BasePresenter<ExtensionPreferencesController>() {
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
}

View File

@ -5,6 +5,21 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/extension_warning_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@color/red_error"
android:gravity="center"
android:padding="16dp"
android:textColor="@android:color/white"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView
android:id="@+id/extension_icon"
android:layout_width="56dp"
@ -13,7 +28,7 @@
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="@id/extension_pkg"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/extension_title" />
app:layout_constraintTop_toBottomOf="@id/extension_warning_banner" />
<TextView
android:id="@+id/extension_title"
@ -23,7 +38,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toEndOf="@id/extension_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/extension_warning_banner"
tools:text="Tachiyomi: Extension" />
<TextView
@ -60,60 +75,32 @@
app:layout_constraintTop_toBottomOf="@id/extension_lang"
tools:text="eu.kanade.tachiyomi.extension.en.myext" />
<TextView
android:id="@+id/extension_warning_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="@color/red_error"
android:gravity="center"
android:padding="16dp"
android:textColor="@android:color/white"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/extension_pkg" />
<Button
android:id="@+id/extension_uninstall_button"
style="@style/Theme.Widget.Button.FilledAccent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/ext_uninstall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/extension_warning_banner" />
app:layout_constraintTop_toBottomOf="@id/extension_lang" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/extension_prefs_recycler"
<TextView
android:id="@+id/extension_prefs"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_height="@dimen/material_component_lists_two_line_height"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/extension_uninstall_button" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/extension_prefs_empty_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:background="@drawable/list_item_selector"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/ext_preferences"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/extension_uninstall_button" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
app:layout_constraintTop_toBottomOf="@id/extension_uninstall_button"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/extension_prefs_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/extension_prefs_empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>