From 13943f77f7c06d7978165a1d9ba139e72dfed853 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 7 May 2022 23:34:55 -0400 Subject: [PATCH] Migrate extension details page to Compose --- .../java/eu/kanade/domain/DomainModule.kt | 2 + .../interactor/GetExtensionSources.kt | 32 +++ .../domain/source/interactor/ToggleSource.kt | 13 +- .../browse/ExtensionDetailsScreen.kt | 237 ++++++++++++++++++ .../{source => browse}/MigrateMangaScreen.kt | 4 +- .../{source => browse}/MigrateSourceScreen.kt | 4 +- .../{source => browse}/SourceFilterScreen.kt | 5 +- .../{source => browse}/SourceScreen.kt | 4 +- .../components/BaseSourceItem.kt | 4 +- .../details/ExtensionDetailsController.kt | 181 ++----------- .../details/ExtensionDetailsHeaderAdapter.kt | 62 ----- .../details/ExtensionDetailsPresenter.kt | 55 +++- .../manga/MigrationMangaController.kt | 2 +- .../sources/MigrationSourcesController.kt | 2 +- .../ui/browse/source/SourceController.kt | 2 +- .../browse/source/SourceFilterController.kt | 2 +- .../ui/browse/source/SourceFilterPresenter.kt | 1 + .../ui/browse/source/SourcePresenter.kt | 2 +- .../util/preference/PreferenceDSL.kt | 5 - .../preference/SwitchSettingsPreference.kt | 34 --- .../layout/extension_detail_controller.xml | 5 - .../res/layout/extension_detail_header.xml | 130 ---------- app/src/main/res/layout/pref_settings.xml | 25 -- app/src/main/res/values/strings.xml | 2 +- 24 files changed, 363 insertions(+), 452 deletions(-) create mode 100644 app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt rename app/src/main/java/eu/kanade/presentation/{source => browse}/MigrateMangaScreen.kt (96%) rename app/src/main/java/eu/kanade/presentation/{source => browse}/MigrateSourceScreen.kt (97%) rename app/src/main/java/eu/kanade/presentation/{source => browse}/SourceFilterScreen.kt (97%) rename app/src/main/java/eu/kanade/presentation/{source => browse}/SourceScreen.kt (98%) rename app/src/main/java/eu/kanade/presentation/{source => browse}/components/BaseSourceItem.kt (92%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchSettingsPreference.kt delete mode 100644 app/src/main/res/layout/extension_detail_controller.xml delete mode 100644 app/src/main/res/layout/extension_detail_header.xml delete mode 100644 app/src/main/res/layout/pref_settings.xml diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 293d3485c..5d5ab0788 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -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 { SourceRepositoryImpl(get(), get()) } diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt new file mode 100644 index 000000000..a2acb4bed --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt @@ -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> { + 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(), + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt index be1ec7a28..585d20b99 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSource.kt @@ -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() } } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt new file mode 100644 index 000000000..fdfd9ece1 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -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) + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/source/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt similarity index 96% rename from app/src/main/java/eu/kanade/presentation/source/MigrateMangaScreen.kt rename to app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt index 4f0bb1872..eaa16dbdd 100644 --- a/app/src/main/java/eu/kanade/presentation/source/MigrateMangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt similarity index 97% rename from app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt rename to app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index 9a529691d..b2dad5c44 100644 --- a/app/src/main/java/eu/kanade/presentation/source/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFilterScreen.kt similarity index 97% rename from app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt rename to app/src/main/java/eu/kanade/presentation/browse/SourceFilterScreen.kt index 09c196f97..b97ce3060 100644 --- a/app/src/main/java/eu/kanade/presentation/source/SourceFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFilterScreen.kt @@ -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(), diff --git a/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceScreen.kt similarity index 98% rename from app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt rename to app/src/main/java/eu/kanade/presentation/browse/SourceScreen.kt index 2c8ab3e85..bfac929bb 100644 --- a/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceScreen.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt similarity index 92% rename from app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt rename to app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt index d5d1b6fc1..fed26e836 100644 --- a/app/src/main/java/eu/kanade/presentation/source/components/BaseSourceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt index 4afa9122d..a355404a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt @@ -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(bundle) { + ComposeController(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) { - 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) { - 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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt deleted file mode 100644 index 897e05c95..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsHeaderAdapter.kt +++ /dev/null @@ -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() { - - 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) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt index bfe30a46a..0a0eecd13 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt @@ -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() { - private val extensionManager: ExtensionManager by injectLazy() - val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } + private val _state: MutableStateFlow> = MutableStateFlow(emptyList()) + val sourcesState: StateFlow> = _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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt index 23509cb78..024acc78d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt index c400c0926..4e1762f16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index ed86c9d11..45d491bda 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt index b68cd3aa8..df3a96bb7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt index e4bec0995..42301b488 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterPresenter.kt @@ -28,6 +28,7 @@ class SourceFilterPresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) + presenterScope.launchIO { getLanguagesWithSources.subscribe() .catch { exception -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt index 9de170ebf..365304da5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceDSL.kt index 772399b1a..70ce375e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceDSL.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchSettingsPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchSettingsPreference.kt deleted file mode 100644 index b5530ada5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchSettingsPreference.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/res/layout/extension_detail_controller.xml b/app/src/main/res/layout/extension_detail_controller.xml deleted file mode 100644 index 321b78630..000000000 --- a/app/src/main/res/layout/extension_detail_controller.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/app/src/main/res/layout/extension_detail_header.xml b/app/src/main/res/layout/extension_detail_header.xml deleted file mode 100644 index 5a7eb8e48..000000000 --- a/app/src/main/res/layout/extension_detail_header.xml +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - -