From 54cfb2acdfbdb29156e8053bdbd3e2fdfe3f6a43 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 31 May 2020 16:23:51 -0400 Subject: [PATCH] Manage sources from extension details (closes #3152) --- .../extension/ExtensionDetailsController.kt | 226 +++++++++++++++++- .../extension/ExtensionFilterController.kt | 12 +- .../ExtensionPreferencesController.kt | 196 --------------- .../ExtensionPreferencesPresenter.kt | 14 -- .../ui/setting/SettingsSourcesController.kt | 75 +----- .../layout/extension_detail_controller.xml | 15 +- .../extension_preferences_controller.xml | 20 -- app/src/main/res/menu/extension_details.xml | 14 ++ app/src/main/res/values/strings.xml | 3 +- 9 files changed, 253 insertions(+), 322 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesPresenter.kt delete mode 100644 app/src/main/res/layout/extension_preferences_controller.xml create mode 100644 app/src/main/res/menu/extension_details.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionDetailsController.kt index 952d583f2..85fe4397a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionDetailsController.kt @@ -1,23 +1,61 @@ 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.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.appcompat.view.ContextThemeWrapper +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.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.source.CatalogueSource 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.withFadeTransaction +import eu.kanade.tachiyomi.util.preference.checkBoxPreference +import eu.kanade.tachiyomi.util.preference.onChange +import eu.kanade.tachiyomi.util.preference.preference +import eu.kanade.tachiyomi.util.preference.preferenceCategory 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 uy.kohesive.injekt.injectLazy +@SuppressLint("RestrictedApi") class ExtensionDetailsController(bundle: Bundle? = null) : - NucleusController(bundle) { + NucleusController(bundle), + PreferenceManager.OnDisplayPreferenceDialogListener, + DialogPreference.TargetFragment { + + private val preferences: PreferencesHelper by injectLazy() + + private var preferenceScreen: PreferenceScreen? = null + private var lastOpenPreferencePosition: Int? = null constructor(pkgName: String) : this( Bundle().apply { @@ -25,8 +63,13 @@ class ExtensionDetailsController(bundle: Bundle? = null) : } ) + init { + setHasOptionsMenu(true) + } + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - binding = ExtensionDetailControllerBinding.inflate(inflater) + val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) + binding = ExtensionDetailControllerBinding.inflate(themedInflater) return binding.root } @@ -65,25 +108,186 @@ class ExtensionDetailsController(bundle: Bundle? = null) : binding.extensionWarningBanner.setText(R.string.unofficial_extension_message) } - if (presenter.extension?.sources?.find { it is ConfigurableSource } != null) { - binding.extensionPrefs.visible() - binding.extensionPrefs.clicks() - .onEach { openPreferences() } - .launchIn(scope) + initPreferences(context, extension) + } + + private fun initPreferences(context: Context, extension: Extension.Installed) { + val themedContext by lazy { getPreferenceThemeContext() } + val manager = PreferenceManager(themedContext) + manager.preferenceDataStore = EmptyPreferenceDataStore() + manager.onDisplayPreferenceDialogListener = this + val screen = manager.createPreferenceScreen(themedContext) + preferenceScreen = screen + + with(screen) { + extension.sources + .groupBy { (it as CatalogueSource).lang } + .toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) }) + .forEach { + preferenceCategory { + title = LocaleHelper.getSourceDisplayName(it.key, context) + it.value + .sortedWith(compareBy({ !it.isEnabled() }, { it.name })) + .forEach { source -> + val sourcePrefs = mutableListOf() + + // Source enable/disable + checkBoxPreference { + key = getSourceKey(source.id) + title = source.toString() + isPersistent = false + isChecked = source.isEnabled() + + onChange { newValue -> + val checked = newValue as Boolean + toggleSource(source, checked) + true + } + + // React to enable/disable all changes + preferences.hiddenCatalogues().asFlow() + .onEach { + val enabled = source.isEnabled() + isChecked = enabled + sourcePrefs.forEach { pref -> pref.isVisible = enabled } + } + .launchIn(scope) + } + + // Source preferences + if (source is ConfigurableSource) { + // TODO + val dataStore = SharedPreferencesDataStore(/*if (source is HttpSource) { + source.preferences + } else {*/ + context.getSharedPreferences(getSourceKey(source.id), Context.MODE_PRIVATE) + /*}*/ + ) + + val newScreen = screen.preferenceManager.createPreferenceScreen(context) + source.setupPreferenceScreen(newScreen) + + // Reparent the preferences + while (newScreen.preferenceCount != 0) { + val pref = newScreen.getPreference(0) + sourcePrefs.add(pref) + + pref.preferenceDataStore = dataStore + pref.order = Int.MAX_VALUE // reset to default order + pref.isVisible = source.isEnabled() + + newScreen.removePreference(pref) + screen.addPreference(pref) + } + } + } + } + } } + + binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context) + binding.extensionPrefsRecycler.adapter = PreferenceGroupAdapter(screen) + binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL)) + } + + 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 + } + + override fun onDestroyView(view: View) { + preferenceScreen = null + super.onDestroyView(view) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.extension_details, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_enable_all -> toggleAllSources(true) + R.id.action_disable_all -> toggleAllSources(false) + } + return super.onOptionsItemSelected(item) } fun onExtensionUninstalled() { router.popCurrentController() } - private fun openPreferences() { - router.pushController( - ExtensionPreferencesController(presenter.extension!!.pkgName).withFadeTransaction() + private fun toggleAllSources(enable: Boolean) { + presenter.extension?.sources?.forEach { toggleSource(it, enable) } + } + + private fun toggleSource(source: Source, enable: Boolean) { + val current = preferences.hiddenCatalogues().get() + + preferences.hiddenCatalogues().set( + if (enable) { + current - source.id.toString() + } else { + current + source.id.toString() + } ) } + private fun Source.isEnabled(): Boolean { + return id.toString() !in preferences.hiddenCatalogues().get() + } + + private fun getSourceKey(sourceId: Long): String { + return "source_$sourceId" + } + + 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 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" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt index a74208fc1..b21940704 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt @@ -36,11 +36,13 @@ class ExtensionFilterController : SettingsController() { val checked = newValue as Boolean val currentActiveLangs = preferences.enabledLanguages().get() - preferences.enabledLanguages().set(if (checked) { - currentActiveLangs + it - } else { - currentActiveLangs - it - }) + preferences.enabledLanguages().set( + if (checked) { + currentActiveLangs + it + } else { + currentActiveLangs - it + } + ) true } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesController.kt deleted file mode 100644 index a36099495..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesController.kt +++ /dev/null @@ -1,196 +0,0 @@ -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(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 - - extension.sources - .filterIsInstance() - .forEach { source -> - 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 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" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesPresenter.kt deleted file mode 100644 index 633c05ff4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPreferencesPresenter.kt +++ /dev/null @@ -1,14 +0,0 @@ -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() { - - val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt index 33d14c1e8..99f5dfd6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt @@ -1,15 +1,11 @@ package eu.kanade.tachiyomi.ui.setting import android.graphics.drawable.Drawable -import androidx.preference.CheckBoxPreference -import androidx.preference.PreferenceGroup import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.icon -import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.preference.onChange -import eu.kanade.tachiyomi.util.preference.switchPreferenceCategory +import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.system.LocaleHelper import java.util.TreeMap @@ -33,28 +29,22 @@ class SettingsSourcesController : SettingsController() { val orderedLangs = sourcesByLang.keys.sortedWith(compareBy({ it !in activeLangsCodes }, { LocaleHelper.getSourceDisplayName(it, context) })) orderedLangs.forEach { lang -> - val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name } - - // Create a preference group and set initial state and change listener - switchPreferenceCategory { + switchPreference { preferenceScreen.addPreference(this) title = LocaleHelper.getSourceDisplayName(lang, context) isPersistent = false - if (lang in activeLangsCodes) { - setChecked(true) - addLanguageSources(this, sources) - } + isChecked = lang in activeLangsCodes onChange { newValue -> val checked = newValue as Boolean val current = preferences.enabledLanguages().get() - if (!checked) { - preferences.enabledLanguages().set(current - lang) - removeAll() - } else { - preferences.enabledLanguages().set(current + lang) - addLanguageSources(this, sources) - } + preferences.enabledLanguages().set( + if (!checked) { + current - lang + } else { + current + lang + } + ) true } } @@ -64,49 +54,4 @@ class SettingsSourcesController : SettingsController() { override fun setDivider(divider: Drawable?) { super.setDivider(null) } - - /** - * Adds the source list for the given group (language). - * - * @param group the language category. - */ - private fun addLanguageSources(group: PreferenceGroup, sources: List) { - val hiddenCatalogues = preferences.hiddenCatalogues().get() - - sources.forEach { source -> - val sourcePreference = CheckBoxPreference(group.context).apply { - val id = source.id.toString() - title = source.name - key = getSourceKey(source.id) - isPersistent = false - isChecked = id !in hiddenCatalogues - - val sourceIcon = source.icon() - if (sourceIcon != null) { - icon = sourceIcon - } - - onChange { newValue -> - val checked = newValue as Boolean - val current = preferences.hiddenCatalogues().get() - - preferences.hiddenCatalogues().set( - if (checked) { - current - id - } else { - current + id - } - ) - - true - } - } - - group.addPreference(sourcePreference) - } - } - - private fun getSourceKey(sourceId: Long): String { - return "source_$sourceId" - } } diff --git a/app/src/main/res/layout/extension_detail_controller.xml b/app/src/main/res/layout/extension_detail_controller.xml index da0689e46..8b8b31d89 100644 --- a/app/src/main/res/layout/extension_detail_controller.xml +++ b/app/src/main/res/layout/extension_detail_controller.xml @@ -88,19 +88,14 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/extension_lang" /> - + app:layout_constraintTop_toBottomOf="@id/extension_uninstall_button" /> diff --git a/app/src/main/res/layout/extension_preferences_controller.xml b/app/src/main/res/layout/extension_preferences_controller.xml deleted file mode 100644 index 46946d40d..000000000 --- a/app/src/main/res/layout/extension_preferences_controller.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/menu/extension_details.xml b/app/src/main/res/menu/extension_details.xml new file mode 100644 index 000000000..f0eae214a --- /dev/null +++ b/app/src/main/res/menu/extension_details.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f40790a94..cc49300f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,8 @@ Delete Update Update library + Enable all + Disable all Edit Add Add category @@ -222,7 +224,6 @@ This extension is not from the official Tachiyomi extensions list. Version: %1$s Language: %1$s - No preferences to edit for this extension Fullscreen