Migrate extension details page to Compose

This commit is contained in:
arkon
2022-05-07 23:34:55 -04:00
parent 1c94ecdcdf
commit 13943f77f7
24 changed files with 363 additions and 452 deletions

View File

@@ -1,59 +1,30 @@
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.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.core.os.bundleOf
import androidx.preference.PreferenceGroupAdapter
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.presentation.browse.ExtensionDetailsScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.switchSettingsPreference
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import okhttp3.HttpUrl.Companion.toHttpUrl
import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle) {
ComposeController<ExtensionDetailsPresenter>(bundle) {
private val preferences: PreferencesHelper by injectLazy()
private val network: NetworkHelper by injectLazy()
private var preferenceScreen: PreferenceScreen? = null
constructor(pkgName: String) : this(
bundleOf(PKGNAME_KEY to pkgName),
)
@@ -62,122 +33,22 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
setHasOptionsMenu(true)
}
override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
return ExtensionDetailControllerBinding.inflate(themedInflater)
}
override fun getTitle() = resources?.getString(R.string.label_extension_info)
override fun createPresenter(): ExtensionDetailsPresenter {
return ExtensionDetailsPresenter(this, args.getString(PKGNAME_KEY)!!)
}
override fun createPresenter() = ExtensionDetailsPresenter(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)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
val extension = presenter.extension ?: return
val context = view.context
binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context)
binding.extensionPrefsRecycler.adapter = ConcatAdapter(
ExtensionDetailsHeaderAdapter(presenter),
initPreferencesAdapter(context, extension),
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
ExtensionDetailsScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter,
onClickUninstall = { presenter.uninstallExtension() },
onClickAppInfo = { presenter.openInSettings() },
onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
onClickSource = { presenter.toggleSource(it) },
)
}
private fun initPreferencesAdapter(context: Context, extension: Extension.Installed): PreferenceGroupAdapter {
val themedContext = getPreferenceThemeContext()
val manager = PreferenceManager(themedContext)
manager.preferenceDataStore = EmptyPreferenceDataStore()
val screen = manager.createPreferenceScreen(themedContext)
preferenceScreen = screen
val isMultiSource = extension.sources.size > 1
val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
with(screen) {
if (isMultiSource && isMultiLangSingleSource.not()) {
multiLanguagePreference(context, extension.sources)
} else {
singleLanguagePreference(context, extension.sources)
}
}
return PreferenceGroupAdapter(screen)
}
private fun PreferenceScreen.singleLanguagePreference(context: Context, sources: List<Source>) {
sources
.map { source -> LocaleHelper.getSourceDisplayName(source.lang, context) to source }
.sortedWith(compareBy({ (_, source) -> !source.isEnabled() }, { (lang, _) -> lang.lowercase() }))
.forEach { (lang, source) ->
sourceSwitchPreference(source, lang)
}
}
private fun PreferenceScreen.multiLanguagePreference(context: Context, sources: List<Source>) {
sources
.groupBy { (it as CatalogueSource).lang }
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
.forEach { entry ->
entry.value
.sortedWith(compareBy({ source -> !source.isEnabled() }, { source -> source.name.lowercase() }))
.forEach { source ->
sourceSwitchPreference(source, source.toString())
}
}
}
private fun PreferenceScreen.sourceSwitchPreference(source: Source, name: String) {
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
key = source.getPreferenceKey()
title = name
isPersistent = false
isChecked = source.isEnabled()
onChange { newValue ->
val checked = newValue as Boolean
toggleSource(source, checked)
true
}
// React to enable/disable all changes
preferences.disabledSources().asFlow()
.onEach {
val enabled = source.isEnabled()
isChecked = enabled
}
.launchIn(viewScope)
}
// Source enable/disable
if (source is ConfigurableSource) {
switchSettingsPreference {
block()
onSettingsClick = View.OnClickListener {
router.pushController(SourcePreferencesController(source.id))
}
}
} else {
switchPreference(block)
}
}
override fun onDestroyView(view: View) {
preferenceScreen = null
super.onDestroyView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.extension_details, menu)
@@ -203,15 +74,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
}
private fun toggleAllSources(enable: Boolean) {
presenter.extension?.sources?.forEach { toggleSource(it, enable) }
}
private fun toggleSource(source: Source, enable: Boolean) {
if (enable) {
preferences.disabledSources() -= source.id.toString()
} else {
preferences.disabledSources() += source.id.toString()
}
presenter.toggleSources(enable)
}
private fun openChangelog() {
@@ -263,16 +126,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
logcat { "Cleared $cleared cookies for: ${urls.joinToString()}" }
}
private fun Source.isEnabled(): Boolean {
return id.toString() !in preferences.disabledSources().get()
}
private fun getPreferenceThemeContext(): Context {
val tv = TypedValue()
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
return ContextThemeWrapper(activity, tv.resourceId)
}
}
private const val PKGNAME_KEY = "pkg_name"

View File

@@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPresenter) :
RecyclerView.Adapter<ExtensionDetailsHeaderAdapter.HeaderViewHolder>() {
private lateinit var binding: ExtensionDetailHeaderBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
binding = ExtensionDetailHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return HeaderViewHolder(binding.root)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
holder.bind()
}
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind() {
val extension = presenter.extension ?: return
val context = view.context
extension.getApplicationIcon(context)?.let { binding.icon.setImageDrawable(it) }
binding.title.text = extension.name
binding.version.text = context.getString(R.string.ext_version_info, extension.versionName)
binding.lang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
binding.nsfw.isVisible = extension.isNsfw
binding.pkgname.text = extension.pkgName
binding.btnUninstall.clicks()
.onEach { presenter.uninstallExtension() }
.launchIn(presenter.presenterScope)
binding.btnAppInfo.clicks()
.onEach { presenter.openInSettings() }
.launchIn(presenter.presenterScope)
if (extension.isObsolete) {
binding.warningBanner.isVisible = true
binding.warningBanner.setText(R.string.obsolete_extension_message)
}
if (extension.isUnofficial) {
binding.warningBanner.isVisible = true
binding.warningBanner.setText(R.string.unofficial_extension_message)
}
}
}
}

View File

@@ -1,27 +1,58 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionDetailsPresenter(
private val controller: ExtensionDetailsController,
private val pkgName: String,
private val context: Application = Injekt.get(),
private val getExtensionSources: GetExtensionSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
) : BasePresenter<ExtensionDetailsController>() {
private val extensionManager: ExtensionManager by injectLazy()
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
private val _state: MutableStateFlow<List<ExtensionSourceItem>> = MutableStateFlow(emptyList())
val sourcesState: StateFlow<List<ExtensionSourceItem>> = _state.asStateFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
val extension = extension ?: return
bindToUninstalledExtension()
presenterScope.launchIO {
getExtensionSources.subscribe(extension)
.map {
it.sortedWith(
compareBy(
{ item -> item.enabled.not() },
{ item -> if (item.labelAsName) item.source.name else LocaleHelper.getSourceDisplayName(item.source.lang, context).lowercase() },
),
)
}
.collectLatest { _state.value = it }
}
}
private fun bindToUninstalledExtension() {
@@ -45,6 +76,20 @@ class ExtensionDetailsPresenter(
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", pkgName, null)
}
controller.startActivity(intent)
view?.startActivity(intent)
}
fun toggleSource(sourceId: Long) {
toggleSource.await(sourceId)
}
fun toggleSources(enable: Boolean) {
extension?.sources?.forEach { toggleSource.await(it.id, enable) }
}
}
data class ExtensionSourceItem(
val source: Source,
val enabled: Boolean,
val labelAsName: Boolean,
)

View File

@@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.core.os.bundleOf
import eu.kanade.presentation.source.MigrateMangaScreen
import eu.kanade.presentation.browse.MigrateMangaScreen
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController

View File

@@ -5,7 +5,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.presentation.source.MigrateSourceScreen
import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController

View File

@@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.source.SourceScreen
import eu.kanade.presentation.browse.SourceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController

View File

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.source.SourceFilterScreen
import eu.kanade.presentation.browse.SourceFilterScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController

View File

@@ -28,6 +28,7 @@ class SourceFilterPresenter(
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
getLanguagesWithSources.subscribe()
.catch { exception ->

View File

@@ -6,7 +6,7 @@ import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.source.SourceUiModel
import eu.kanade.presentation.browse.SourceUiModel
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow