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 package eu.kanade.tachiyomi.ui.browse.extension
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.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.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.NucleusController 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.system.LocaleHelper
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) : class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle), NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle) {
PreferenceManager.OnDisplayPreferenceDialogListener,
DialogPreference.TargetFragment {
private var lastOpenPreferencePosition: Int? = null
private var preferenceScreen: PreferenceScreen? = null
constructor(pkgName: String) : this( constructor(pkgName: String) : this(
Bundle().apply { Bundle().apply {
@ -54,8 +26,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
) )
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) binding = ExtensionDetailControllerBinding.inflate(inflater)
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
return binding.root return binding.root
} }
@ -93,132 +64,25 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message) binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
} }
val themedContext by lazy { getPreferenceThemeContext() } if (presenter.extension?.sources?.find { it is ConfigurableSource } != null) {
val manager = PreferenceManager(themedContext) binding.extensionPrefs.visible()
manager.preferenceDataStore = EmptyPreferenceDataStore() binding.extensionPrefs.clicks()
manager.onDisplayPreferenceDialogListener = this .onEach { openPreferences() }
val screen = manager.createPreferenceScreen(themedContext) .launchIn(scope)
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)
} }
fun onExtensionUninstalled() { fun onExtensionUninstalled() {
router.popCurrentController() router.popCurrentController()
} }
override fun onSaveInstanceState(outState: Bundle) { private fun openPreferences() {
lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) } router.pushController(
super.onSaveInstanceState(outState) ExtensionPreferencesController(presenter.extension!!.pkgName).withFadeTransaction()
}
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 { private companion object {
const val PKGNAME_KEY = "pkg_name" 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_width="match_parent"
android:layout_height="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 <ImageView
android:id="@+id/extension_icon" android:id="@+id/extension_icon"
android:layout_width="56dp" android:layout_width="56dp"
@ -13,7 +28,7 @@
android:src="@mipmap/ic_launcher" android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="@id/extension_pkg" app:layout_constraintBottom_toBottomOf="@id/extension_pkg"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/extension_title" /> app:layout_constraintTop_toBottomOf="@id/extension_warning_banner" />
<TextView <TextView
android:id="@+id/extension_title" android:id="@+id/extension_title"
@ -23,7 +38,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
app:layout_constraintStart_toEndOf="@id/extension_icon" app:layout_constraintStart_toEndOf="@id/extension_icon"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@id/extension_warning_banner"
tools:text="Tachiyomi: Extension" /> tools:text="Tachiyomi: Extension" />
<TextView <TextView
@ -60,60 +75,32 @@
app:layout_constraintTop_toBottomOf="@id/extension_lang" app:layout_constraintTop_toBottomOf="@id/extension_lang"
tools:text="eu.kanade.tachiyomi.extension.en.myext" /> 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 <Button
android:id="@+id/extension_uninstall_button" android:id="@+id/extension_uninstall_button"
style="@style/Theme.Widget.Button.FilledAccent" style="@style/Theme.Widget.Button.FilledAccent"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginTop="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/ext_uninstall" android:text="@string/ext_uninstall"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline" app:layout_constraintTop_toBottomOf="@id/extension_lang" />
app:layout_constraintTop_toBottomOf="@id/extension_warning_banner" />
<androidx.recyclerview.widget.RecyclerView <TextView
android:id="@+id/extension_prefs_recycler" android:id="@+id/extension_prefs"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="@dimen/material_component_lists_two_line_height"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:background="@drawable/list_item_selector"
app:layout_constraintEnd_toEndOf="parent" android:gravity="center_vertical"
app:layout_constraintStart_toStartOf="parent" android:padding="16dp"
app:layout_constraintTop_toBottomOf="@id/extension_uninstall_button" /> android:text="@string/ext_preferences"
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/extension_prefs_empty_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/extension_uninstall_button" /> app:layout_constraintTop_toBottomOf="@id/extension_uninstall_button"
tools:visibility="visible" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout> </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>