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

@ -3,6 +3,7 @@ package eu.kanade.domain
import eu.kanade.data.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl
import eu.kanade.data.source.SourceRepositoryImpl
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.extension.interactor.GetExtensionUpdates
import eu.kanade.domain.extension.interactor.GetExtensions
import eu.kanade.domain.history.interactor.DeleteHistoryTable
@ -43,6 +44,7 @@ class DomainModule : InjektModule {
addFactory { RemoveHistoryByMangaId(get()) }
addFactory { GetExtensions(get(), get()) }
addFactory { GetExtensionSources(get()) }
addFactory { GetExtensionUpdates(get(), get()) }
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }

View File

@ -0,0 +1,32 @@
package eu.kanade.domain.extension.interactor
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetExtensionSources(
private val preferences: PreferencesHelper,
) {
fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
val isMultiSource = extension.sources.size > 1
val isMultiLangSingleSource =
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
return preferences.disabledSources().asFlow().map { disabledSources ->
fun Source.isEnabled() = id.toString() !in disabledSources
extension.sources
.map { source ->
ExtensionSourceItem(
source = source,
enabled = source.isEnabled(),
labelAsName = isMultiSource && isMultiLangSingleSource.not(),
)
}
}
}
}

View File

@ -9,12 +9,15 @@ class ToggleSource(
private val preferences: PreferencesHelper,
) {
fun await(source: Source) {
val isEnabled = source.id.toString() !in preferences.disabledSources().get()
if (isEnabled) {
preferences.disabledSources() += source.id.toString()
fun await(source: Source, enable: Boolean = source.id.toString() in preferences.disabledSources().get()) {
await(source.id, enable)
}
fun await(sourceId: Long, enable: Boolean = sourceId.toString() in preferences.disabledSources().get()) {
if (enable) {
preferences.disabledSources() -= sourceId.toString()
} else {
preferences.disabledSources() -= source.id.toString()
preferences.disabledSources() += sourceId.toString()
}
}
}

View File

@ -0,0 +1,237 @@
package eu.kanade.presentation.browse
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsPresenter
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
fun ExtensionDetailsScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionDetailsPresenter,
onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) {
val extension = presenter.extension
if (extension == null) {
EmptyScreen(textResource = R.string.empty_screen)
return
}
val sources by presenter.sourcesState.collectAsState()
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
if (extension.isObsolete) {
item {
WarningBanner(R.string.obsolete_extension_message)
}
}
if (extension.isUnofficial) {
item {
WarningBanner(R.string.unofficial_extension_message)
}
}
item {
DetailsHeader(extension, onClickUninstall, onClickAppInfo)
}
items(
items = sources,
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,
)
}
}
}
@Composable
private fun WarningBanner(@StringRes textRes: Int) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.error)
.padding(16.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(textRes),
color = MaterialTheme.colorScheme.onError,
)
}
}
@Composable
private fun DetailsHeader(
extension: Extension,
onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit,
) {
val context = LocalContext.current
Column {
Row(
modifier = Modifier.padding(
start = horizontalPadding,
end = horizontalPadding,
top = 16.dp,
bottom = 8.dp,
),
) {
ExtensionIcon(
modifier = Modifier
.height(56.dp)
.width(56.dp),
extension = extension,
)
Column(
modifier = Modifier.padding(start = 16.dp),
) {
Text(
text = extension.name,
style = MaterialTheme.typography.titleMedium,
)
Text(
text = stringResource(R.string.ext_version_info, extension.versionName),
style = MaterialTheme.typography.bodySmall,
)
Text(
text = stringResource(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context)),
style = MaterialTheme.typography.bodySmall,
)
if (extension.isNsfw) {
Text(
text = stringResource(R.string.ext_nsfw_warning),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
)
}
Text(
text = extension.pkgName,
style = MaterialTheme.typography.bodySmall,
)
}
}
Row(
modifier = Modifier.padding(
start = horizontalPadding,
end = horizontalPadding,
top = 8.dp,
bottom = 16.dp,
),
) {
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onClickUninstall,
) {
Text(stringResource(R.string.ext_uninstall))
}
Spacer(Modifier.width(16.dp))
Button(
modifier = Modifier.weight(1f),
onClick = onClickAppInfo,
) {
Text(
text = stringResource(R.string.ext_app_info),
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
Divider()
}
}
@Composable
private fun SourceSwitchPreference(
modifier: Modifier = Modifier,
source: ExtensionSourceItem,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) {
val context = LocalContext.current
PreferenceRow(
modifier = modifier,
title = if (source.labelAsName) {
source.source.toString()
} else {
LocaleHelper.getSourceDisplayName(source.source.lang, context)
},
onClick = { onClickSource(source.source.id) },
action = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (source.source is ConfigurableSource) {
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.label_settings),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
Switch(checked = source.enabled, onCheckedChange = null)
}
},
)
}

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.source
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@ -51,7 +51,7 @@ fun MigrateMangaContent(
onClickCover: (Manga) -> Unit,
) {
if (list.isEmpty()) {
EmptyScreen(textResource = R.string.migrate_empty_screen)
EmptyScreen(textResource = R.string.empty_screen)
return
}
LazyColumn(

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.source
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@ -17,10 +17,10 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.ItemBadges
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.source.components.BaseSourceItem
import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.source
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@ -16,10 +16,10 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.source.components.BaseSourceItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
import eu.kanade.tachiyomi.ui.browse.source.SourceFilterPresenter
@ -59,6 +59,7 @@ fun SourceFilterContent(
EmptyScreen(textResource = R.string.source_filter_empty_screen)
return
}
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.source
package eu.kanade.presentation.browse
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -32,9 +32,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.source.components.BaseSourceItem
import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.source.components
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
@ -9,8 +9,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.util.system.LocaleHelper

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

View File

@ -23,7 +23,6 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.preference.AdaptiveTitlePreferenceCategory
import eu.kanade.tachiyomi.widget.preference.IntListPreference
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
import eu.kanade.tachiyomi.widget.preference.SwitchSettingsPreference
@DslMarker
@Target(AnnotationTarget.TYPE)
@ -56,10 +55,6 @@ inline fun PreferenceGroup.switchPreferenceCategory(block: (@DSL SwitchPreferenc
return initThenAdd(SwitchPreferenceCategory(context), block)
}
inline fun PreferenceGroup.switchSettingsPreference(block: (@DSL SwitchSettingsPreference).() -> Unit): SwitchSettingsPreference {
return initThenAdd(SwitchSettingsPreference(context), block)
}
inline fun PreferenceGroup.checkBoxPreference(block: (@DSL CheckBoxPreference).() -> Unit): CheckBoxPreference {
return initThenAdd(CheckBoxPreference(context), block)
}

View File

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.widget.preference
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.R
class SwitchSettingsPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
SwitchPreferenceCompat(context, attrs) {
var onSettingsClick: View.OnClickListener? = null
init {
widgetLayoutResource = R.layout.pref_settings
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
holder.findViewById(R.id.button).setOnClickListener {
onSettingsClick?.onClick(it)
}
// Disable swiping to align with SwitchPreferenceCompat
holder.findViewById(R.id.switchWidget).setOnTouchListener { _, event ->
event.actionMasked == MotionEvent.ACTION_MOVE
}
}
}

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/extension_prefs_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@ -1,130 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/warning_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorError"
android:gravity="center"
android:padding="16dp"
android:textColor="?attr/colorOnError"
android:visibility="gone"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="@id/pkgname"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:elevation="3dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Tachiyomi: Extension" />
<TextView
android:id="@+id/version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:elevation="3dp"
android:gravity="center"
android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="Version: 1.0.0" />
<TextView
android:id="@+id/lang"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:elevation="3dp"
android:gravity="center"
android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/version"
tools:text="Language: English" />
<TextView
android:id="@+id/nsfw"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:elevation="3dp"
android:gravity="center"
android:text="@string/ext_nsfw_warning"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorError"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/lang"
tools:visibility="visible" />
<TextView
android:id="@+id/pkgname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:elevation="3dp"
android:ellipsize="middle"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/nsfw"
tools:text="eu.kanade.tachiyomi.extension.en.myext" />
<Button
android:id="@+id/btn_uninstall"
style="@style/Widget.Tachiyomi.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="4dp"
android:text="@string/ext_uninstall"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_app_info"
app:layout_constraintTop_toBottomOf="@id/pkgname" />
<Button
android:id="@+id/btn_app_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="4dp"
android:text="@string/ext_app_info"
app:layout_constraintStart_toEndOf="@+id/btn_uninstall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/pkgname" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageButton
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/label_settings"
android:padding="8dp"
app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnBackground" />
<!-- Matches ID used in SwitchPreferenceCompat -->
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -730,7 +730,7 @@
<string name="migration_selection_prompt">Select a source to migrate from</string>
<string name="migrate">Migrate</string>
<string name="copy">Copy</string>
<string name="migrate_empty_screen">Well, this is awkward</string>
<string name="empty_screen">Well, this is awkward</string>
<!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>