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.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl import eu.kanade.data.manga.MangaRepositoryImpl
import eu.kanade.data.source.SourceRepositoryImpl 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.GetExtensionUpdates
import eu.kanade.domain.extension.interactor.GetExtensions import eu.kanade.domain.extension.interactor.GetExtensions
import eu.kanade.domain.history.interactor.DeleteHistoryTable import eu.kanade.domain.history.interactor.DeleteHistoryTable
@ -43,6 +44,7 @@ class DomainModule : InjektModule {
addFactory { RemoveHistoryByMangaId(get()) } addFactory { RemoveHistoryByMangaId(get()) }
addFactory { GetExtensions(get(), get()) } addFactory { GetExtensions(get(), get()) }
addFactory { GetExtensionSources(get()) }
addFactory { GetExtensionUpdates(get(), get()) } addFactory { GetExtensionUpdates(get(), get()) }
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(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, private val preferences: PreferencesHelper,
) { ) {
fun await(source: Source) { fun await(source: Source, enable: Boolean = source.id.toString() in preferences.disabledSources().get()) {
val isEnabled = source.id.toString() !in preferences.disabledSources().get() await(source.id, enable)
if (isEnabled) { }
preferences.disabledSources() += source.id.toString()
fun await(sourceId: Long, enable: Boolean = sourceId.toString() in preferences.disabledSources().get()) {
if (enable) {
preferences.disabledSources() -= sourceId.toString()
} else { } 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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
@ -51,7 +51,7 @@ fun MigrateMangaContent(
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
) { ) {
if (list.isEmpty()) { if (list.isEmpty()) {
EmptyScreen(textResource = R.string.migrate_empty_screen) EmptyScreen(textResource = R.string.empty_screen)
return return
} }
LazyColumn( 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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.model.Source 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.EmptyScreen
import eu.kanade.presentation.components.ItemBadges import eu.kanade.presentation.components.ItemBadges
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.source.components.BaseSourceItem
import eu.kanade.presentation.theme.header import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus 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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues 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.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.source.model.Source 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.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.source.components.BaseSourceItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
import eu.kanade.tachiyomi.ui.browse.source.SourceFilterPresenter import eu.kanade.tachiyomi.ui.browse.source.SourceFilterPresenter
@ -59,6 +59,7 @@ fun SourceFilterContent(
EmptyScreen(textResource = R.string.source_filter_empty_screen) EmptyScreen(textResource = R.string.source_filter_empty_screen)
return return
} }
LazyColumn( LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop), modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(), 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.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -32,9 +32,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source 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.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.source.components.BaseSourceItem
import eu.kanade.presentation.theme.header import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus 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.Column
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
@ -9,8 +9,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.model.Source 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.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper

View File

@ -1,59 +1,30 @@
package eu.kanade.tachiyomi.ui.browse.extension.details package eu.kanade.tachiyomi.ui.browse.extension.details
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.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import androidx.compose.runtime.Composable
import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.preference.PreferenceGroupAdapter import eu.kanade.presentation.browse.ExtensionDetailsScreen
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.tachiyomi.R 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.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.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.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.pushController 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 eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) : 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 val network: NetworkHelper by injectLazy()
private var preferenceScreen: PreferenceScreen? = null
constructor(pkgName: String) : this( constructor(pkgName: String) : this(
bundleOf(PKGNAME_KEY to pkgName), bundleOf(PKGNAME_KEY to pkgName),
) )
@ -62,122 +33,22 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding { override fun getTitle() = resources?.getString(R.string.label_extension_info)
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
return ExtensionDetailControllerBinding.inflate(themedInflater)
}
override fun createPresenter(): ExtensionDetailsPresenter { override fun createPresenter() = ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
return ExtensionDetailsPresenter(this, args.getString(PKGNAME_KEY)!!)
}
override fun getTitle(): String? { @Composable
return resources?.getString(R.string.label_extension_info) override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
} ExtensionDetailsScreen(
nestedScrollInterop = nestedScrollInterop,
@SuppressLint("PrivateResource") presenter = presenter,
override fun onViewCreated(view: View) { onClickUninstall = { presenter.uninstallExtension() },
super.onViewCreated(view) onClickAppInfo = { presenter.openInSettings() },
onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
binding.extensionPrefsRecycler.applyInsetter { onClickSource = { presenter.toggleSource(it) },
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),
) )
} }
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) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.extension_details, menu) inflater.inflate(R.menu.extension_details, menu)
@ -203,15 +74,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
} }
private fun toggleAllSources(enable: Boolean) { private fun toggleAllSources(enable: Boolean) {
presenter.extension?.sources?.forEach { toggleSource(it, enable) } presenter.toggleSources(enable)
}
private fun toggleSource(source: Source, enable: Boolean) {
if (enable) {
preferences.disabledSources() -= source.id.toString()
} else {
preferences.disabledSources() += source.id.toString()
}
} }
private fun openChangelog() { private fun openChangelog() {
@ -263,16 +126,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
logcat { "Cleared $cleared cookies for: ${urls.joinToString()}" } 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" 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 package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings 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.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter 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 rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionDetailsPresenter( class ExtensionDetailsPresenter(
private val controller: ExtensionDetailsController,
private val pkgName: String, 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>() { ) : BasePresenter<ExtensionDetailsController>() {
private val extensionManager: ExtensionManager by injectLazy()
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } 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?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
val extension = extension ?: return
bindToUninstalledExtension() 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() { private fun bindToUninstalledExtension() {
@ -45,6 +76,20 @@ class ExtensionDetailsPresenter(
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", pkgName, null) 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.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.core.os.bundleOf 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.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController

View File

@ -5,7 +5,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 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.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController 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.runtime.LaunchedEffect
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.domain.source.model.Source 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.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController 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.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import eu.kanade.domain.source.model.Source 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.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController import eu.kanade.tachiyomi.ui.base.controller.ComposeController

View File

@ -28,6 +28,7 @@ class SourceFilterPresenter(
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
presenterScope.launchIO { presenterScope.launchIO {
getLanguagesWithSources.subscribe() getLanguagesWithSources.subscribe()
.catch { exception -> .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.interactor.ToggleSourcePin
import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source 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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow 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.AdaptiveTitlePreferenceCategory
import eu.kanade.tachiyomi.widget.preference.IntListPreference import eu.kanade.tachiyomi.widget.preference.IntListPreference
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
import eu.kanade.tachiyomi.widget.preference.SwitchSettingsPreference
@DslMarker @DslMarker
@Target(AnnotationTarget.TYPE) @Target(AnnotationTarget.TYPE)
@ -56,10 +55,6 @@ inline fun PreferenceGroup.switchPreferenceCategory(block: (@DSL SwitchPreferenc
return initThenAdd(SwitchPreferenceCategory(context), block) 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 { inline fun PreferenceGroup.checkBoxPreference(block: (@DSL CheckBoxPreference).() -> Unit): CheckBoxPreference {
return initThenAdd(CheckBoxPreference(context), block) 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="migration_selection_prompt">Select a source to migrate from</string>
<string name="migrate">Migrate</string> <string name="migrate">Migrate</string>
<string name="copy">Copy</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 --> <!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string> <string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>