From f1b85ff39d13aa81a835a97efb7a70c3d877e115 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 20 Nov 2022 20:36:03 +0100 Subject: [PATCH] Use Voyager on Extension Details screen (#8576) --- .../interactor/GetExtensionSources.kt | 7 +- .../browse/ExtensionDetailsScreen.kt | 154 ++++++++-------- .../browse/ExtensionDetailsState.kt | 25 --- .../details/ExtensionDetailsController.kt | 34 ++-- .../details/ExtensionDetailsPresenter.kt | 145 --------------- .../details/ExtensionDetailsScreen.kt | 57 ++++++ .../details/ExtensionDetailsScreenModel.kt | 167 ++++++++++++++++++ 7 files changed, 325 insertions(+), 264 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt 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 index ba77bbc46b..280dfae04a 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionSources.kt @@ -3,7 +3,6 @@ package eu.kanade.domain.extension.interactor import eu.kanade.domain.source.service.SourcePreferences 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 @@ -30,3 +29,9 @@ class GetExtensionSources( } } } + +data class ExtensionSourceItem( + val source: Source, + val enabled: Boolean, + val labelAsName: Boolean, +) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index b092f8f5a6..f8f1562586 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -38,19 +38,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import eu.kanade.domain.extension.interactor.ExtensionSourceItem import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.DIVIDER_ALPHA import eu.kanade.presentation.components.Divider import eu.kanade.presentation.components.EmptyScreen -import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.WarningBanner @@ -60,18 +59,22 @@ import eu.kanade.presentation.util.padding 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.ui.browse.extension.details.ExtensionDetailsState import eu.kanade.tachiyomi.util.system.LocaleHelper @Composable fun ExtensionDetailsScreen( navigateUp: () -> Unit, - presenter: ExtensionDetailsPresenter, + state: ExtensionDetailsState, onClickSourcePreferences: (sourceId: Long) -> Unit, + onClickWhatsNew: () -> Unit, + onClickReadme: () -> Unit, + onClickEnableAll: () -> Unit, + onClickDisableAll: () -> Unit, + onClickClearCookies: () -> Unit, + onClickUninstall: () -> Unit, + onClickSource: (sourceId: Long) -> Unit, ) { - val uriHandler = LocalUriHandler.current - Scaffold( topBar = { scrollBehavior -> AppBar( @@ -80,19 +83,19 @@ fun ExtensionDetailsScreen( actions = { AppBarActions( actions = buildList { - if (presenter.extension?.isUnofficial == false) { + if (state.extension?.isUnofficial == false) { add( AppBar.Action( title = stringResource(R.string.whats_new), icon = Icons.Outlined.History, - onClick = { uriHandler.openUri(presenter.getChangelogUrl()) }, + onClick = onClickWhatsNew, ), ) add( AppBar.Action( title = stringResource(R.string.action_faq_and_guides), icon = Icons.Outlined.HelpOutline, - onClick = { uriHandler.openUri(presenter.getReadmeUrl()) }, + onClick = onClickReadme, ), ) } @@ -100,15 +103,15 @@ fun ExtensionDetailsScreen( listOf( AppBar.OverflowAction( title = stringResource(R.string.action_enable_all), - onClick = { presenter.toggleSources(true) }, + onClick = onClickEnableAll, ), AppBar.OverflowAction( title = stringResource(R.string.action_disable_all), - onClick = { presenter.toggleSources(false) }, + onClick = onClickDisableAll, ), AppBar.OverflowAction( title = stringResource(R.string.pref_clear_cookies), - onClick = { presenter.clearCookies() }, + onClick = onClickClearCookies, ), ), ) @@ -119,77 +122,86 @@ fun ExtensionDetailsScreen( ) }, ) { paddingValues -> - ExtensionDetails(paddingValues, presenter, onClickSourcePreferences) + + if (state.extension == null) { + EmptyScreen( + textResource = R.string.empty_screen, + modifier = Modifier.padding(paddingValues), + ) + return@Scaffold + } + + ExtensionDetails( + contentPadding = paddingValues, + extension = state.extension, + sources = state.sources, + onClickSourcePreferences = onClickSourcePreferences, + onClickUninstall = onClickUninstall, + onClickSource = onClickSource, + ) } } @Composable private fun ExtensionDetails( contentPadding: PaddingValues, - presenter: ExtensionDetailsPresenter, + extension: Extension.Installed, + sources: List, onClickSourcePreferences: (sourceId: Long) -> Unit, + onClickUninstall: () -> Unit, + onClickSource: (sourceId: Long) -> Unit, ) { - when { - presenter.isLoading -> LoadingScreen() - presenter.extension == null -> EmptyScreen( - textResource = R.string.empty_screen, - modifier = Modifier.padding(contentPadding), - ) - else -> { - val context = LocalContext.current - val extension = presenter.extension - var showNsfwWarning by remember { mutableStateOf(false) } - - ScrollbarLazyColumn( - contentPadding = contentPadding, - ) { - when { - extension.isUnofficial -> - item { - WarningBanner(R.string.unofficial_extension_message) - } - extension.isObsolete -> - item { - WarningBanner(R.string.obsolete_extension_message) - } - } + val context = LocalContext.current + var showNsfwWarning by remember { mutableStateOf(false) } + ScrollbarLazyColumn( + contentPadding = contentPadding, + ) { + when { + extension.isUnofficial -> item { - DetailsHeader( - extension = extension, - onClickUninstall = { presenter.uninstallExtension() }, - onClickAppInfo = { - Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", extension.pkgName, null) - context.startActivity(this) - } - }, - onClickAgeRating = { - showNsfwWarning = true - }, - ) + WarningBanner(R.string.unofficial_extension_message) } - - items( - items = presenter.sources, - key = { it.source.id }, - ) { source -> - SourceSwitchPreference( - modifier = Modifier.animateItemPlacement(), - source = source, - onClickSourcePreferences = onClickSourcePreferences, - onClickSource = { presenter.toggleSource(it) }, - ) + extension.isObsolete -> + item { + WarningBanner(R.string.obsolete_extension_message) } - } - if (showNsfwWarning) { - NsfwWarningDialog( - onClickConfirm = { - showNsfwWarning = false - }, - ) - } } + + item { + DetailsHeader( + extension = extension, + onClickUninstall = onClickUninstall, + onClickAppInfo = { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", extension.pkgName, null) + context.startActivity(this) + } + }, + onClickAgeRating = { + showNsfwWarning = true + }, + ) + } + + items( + items = sources, + key = { it.source.id }, + ) { source -> + SourceSwitchPreference( + modifier = Modifier.animateItemPlacement(), + source = source, + onClickSourcePreferences = onClickSourcePreferences, + onClickSource = onClickSource, + ) + } + } + if (showNsfwWarning) { + NsfwWarningDialog( + onClickConfirm = { + showNsfwWarning = false + }, + ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt deleted file mode 100644 index 1b229e272d..0000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package eu.kanade.presentation.browse - -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import eu.kanade.tachiyomi.extension.model.Extension -import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem - -@Stable -interface ExtensionDetailsState { - val isLoading: Boolean - val extension: Extension.Installed? - val sources: List -} - -fun ExtensionDetailsState(): ExtensionDetailsState { - return ExtensionDetailsStateImpl() -} - -class ExtensionDetailsStateImpl : ExtensionDetailsState { - override var isLoading: Boolean by mutableStateOf(true) - override var extension: Extension.Installed? by mutableStateOf(null) - override var sources: List by mutableStateOf(emptyList()) -} 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 844852ebac..fa136ec4b5 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,36 +1,26 @@ package eu.kanade.tachiyomi.ui.browse.extension.details -import android.annotation.SuppressLint import android.os.Bundle import androidx.compose.runtime.Composable import androidx.core.os.bundleOf +import cafe.adriel.voyager.navigator.Navigator import eu.kanade.presentation.browse.ExtensionDetailsScreen -import eu.kanade.tachiyomi.ui.base.controller.FullComposeController -import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController -@SuppressLint("RestrictedApi") -class ExtensionDetailsController( - bundle: Bundle? = null, -) : FullComposeController(bundle) { +private const val PKGNAME_KEY = "pkg_name" - constructor(pkgName: String) : this( - bundleOf(PKGNAME_KEY to pkgName), - ) +class ExtensionDetailsController : BasicFullComposeController { - override fun createPresenter() = ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!) + @Suppress("unused") + constructor(bundle: Bundle) : this(bundle.getString(PKGNAME_KEY)!!) + + constructor(pkgName: String) : super(bundleOf(PKGNAME_KEY to pkgName)) + + val pkgName: String + get() = args.getString(PKGNAME_KEY)!! @Composable override fun ComposeContent() { - ExtensionDetailsScreen( - navigateUp = router::popCurrentController, - presenter = presenter, - onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) }, - ) - } - - fun onExtensionUninstalled() { - router.popCurrentController() + Navigator(screen = ExtensionDetailsScreen(pkgName = pkgName)) } } - -private const val PKGNAME_KEY = "pkg_name" 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 deleted file mode 100644 index 1ddc124c58..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt +++ /dev/null @@ -1,145 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.extension.details - -import android.app.Application -import android.os.Bundle -import eu.kanade.domain.extension.interactor.GetExtensionSources -import eu.kanade.domain.source.interactor.ToggleSource -import eu.kanade.presentation.browse.ExtensionDetailsState -import eu.kanade.presentation.browse.ExtensionDetailsStateImpl -import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.withUIContext -import eu.kanade.tachiyomi.util.system.LocaleHelper -import eu.kanade.tachiyomi.util.system.logcat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map -import okhttp3.HttpUrl.Companion.toHttpUrl -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionDetailsPresenter( - private val pkgName: String, - private val state: ExtensionDetailsStateImpl = ExtensionDetailsState() as ExtensionDetailsStateImpl, - private val context: Application = Injekt.get(), - private val getExtensionSources: GetExtensionSources = Injekt.get(), - private val toggleSource: ToggleSource = Injekt.get(), - private val network: NetworkHelper = Injekt.get(), - private val extensionManager: ExtensionManager = Injekt.get(), -) : BasePresenter(), ExtensionDetailsState by state { - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - presenterScope.launchIO { - extensionManager.installedExtensionsFlow - .map { it.firstOrNull { pkg -> pkg.pkgName == pkgName } } - .collectLatest { extension -> - // If extension is null it's most likely uninstalled - if (extension == null) { - withUIContext { - view?.onExtensionUninstalled() - } - return@collectLatest - } - state.extension = extension - fetchExtensionSources() - } - } - } - - private fun CoroutineScope.fetchExtensionSources() { - 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.isLoading = false - state.sources = it - } - } - } - - fun getChangelogUrl(): String { - extension ?: return "" - - val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") - val pkgFactory = extension.pkgFactory - if (extension.hasChangelog) { - return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/CHANGELOG.md") - } - - // Falling back on GitHub commit history because there is no explicit changelog in extension - return createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory) - } - - fun getReadmeUrl(): String { - extension ?: return "" - - if (!extension.hasReadme) { - return "https://tachiyomi.org/help/faq/#extensions" - } - - val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") - val pkgFactory = extension.pkgFactory - return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/README.md") - } - - fun clearCookies() { - val urls = extension?.sources - ?.filterIsInstance() - ?.map { it.baseUrl } - ?.distinct() ?: emptyList() - - val cleared = urls.sumOf { - network.cookieManager.remove(it.toHttpUrl()) - } - - logcat { "Cleared $cleared cookies for: ${urls.joinToString()}" } - } - - fun uninstallExtension() { - val extension = extension ?: return - extensionManager.uninstallExtension(extension.pkgName) - } - - fun toggleSource(sourceId: Long) { - toggleSource.await(sourceId) - } - - fun toggleSources(enable: Boolean) { - extension?.sources - ?.map { it.id } - ?.let { toggleSource.await(it, enable) } - } - - private fun createUrl(url: String, pkgName: String, pkgFactory: String?, path: String = ""): String { - return if (!pkgFactory.isNullOrEmpty()) { - when (path.isEmpty()) { - true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory" - else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path - } - } else { - url + "/src/" + pkgName.replace(".", "/") + path - } - } -} - -data class ExtensionSourceItem( - val source: Source, - val enabled: Boolean, - val labelAsName: Boolean, -) - -private const val URL_EXTENSION_COMMITS = "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master" -private const val URL_EXTENSION_BLOB = "https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt new file mode 100644 index 0000000000..b5aca286d9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt @@ -0,0 +1,57 @@ +package eu.kanade.tachiyomi.ui.browse.extension.details + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.browse.ExtensionDetailsScreen +import eu.kanade.presentation.components.LoadingScreen +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.ui.base.controller.pushController +import kotlinx.coroutines.flow.collectLatest + +class ExtensionDetailsScreen( + private val pkgName: String, +) : Screen { + + @Composable + override fun Content() { + val context = LocalContext.current + val screenModel = rememberScreenModel { ExtensionDetailsScreenModel(pkgName = pkgName, context = context) } + val state by screenModel.state.collectAsState() + + if (state.isLoading) { + LoadingScreen() + return + } + + val router = LocalRouter.currentOrThrow + val uriHandler = LocalUriHandler.current + + ExtensionDetailsScreen( + navigateUp = router::popCurrentController, + state = state, + onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) }, + onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) }, + onClickReadme = { uriHandler.openUri(screenModel.getReadmeUrl()) }, + onClickEnableAll = { screenModel.toggleSources(true) }, + onClickDisableAll = { screenModel.toggleSources(false) }, + onClickClearCookies = { screenModel.clearCookies() }, + onClickUninstall = { screenModel.uninstallExtension() }, + onClickSource = { screenModel.toggleSource(it) }, + ) + + LaunchedEffect(Unit) { + screenModel.events.collectLatest { event -> + if (event is ExtensionDetailsEvent.Uninstalled) { + router.popCurrentController() + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt new file mode 100644 index 0000000000..26c907d8f3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt @@ -0,0 +1,167 @@ +package eu.kanade.tachiyomi.ui.browse.extension.details + +import android.content.Context +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.extension.interactor.ExtensionSourceItem +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.model.Extension +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import okhttp3.HttpUrl.Companion.toHttpUrl +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +private const val URL_EXTENSION_COMMITS = + "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master" +private const val URL_EXTENSION_BLOB = + "https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master" + +class ExtensionDetailsScreenModel( + pkgName: String, + context: Context, + private val network: NetworkHelper = Injekt.get(), + private val extensionManager: ExtensionManager = Injekt.get(), + private val getExtensionSources: GetExtensionSources = Injekt.get(), + private val toggleSource: ToggleSource = Injekt.get(), +) : StateScreenModel(ExtensionDetailsState()) { + + private val _events: Channel = Channel() + val events: Flow = _events.receiveAsFlow() + + init { + coroutineScope.launch { + launch { + extensionManager.installedExtensionsFlow + .map { it.firstOrNull { extension -> extension.pkgName == pkgName } } + .collectLatest { extension -> + if (extension == null) { + _events.send(ExtensionDetailsEvent.Uninstalled) + return@collectLatest + } + mutableState.update { state -> + state.copy(extension = extension) + } + } + } + launch { + state.collectLatest { state -> + if (state.extension == null) return@collectLatest + getExtensionSources.subscribe(state.extension) + .map { + it.sortedWith( + compareBy( + { !it.enabled }, + { item -> + item.source.name.takeIf { item.labelAsName } + ?: LocaleHelper.getSourceDisplayName(item.source.lang, context).lowercase() + }, + ), + ) + }.collectLatest { sources -> + mutableState.update { + it.copy( + sources = sources, + ) + } + } + } + } + } + } + + fun getChangelogUrl(): String { + val extension = state.value.extension ?: return "" + + val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") + val pkgFactory = extension.pkgFactory + if (extension.hasChangelog) { + return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/CHANGELOG.md") + } + + // Falling back on GitHub commit history because there is no explicit changelog in extension + return createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory) + } + + fun getReadmeUrl(): String { + val extension = state.value.extension ?: return "" + + if (!extension.hasReadme) { + return "https://tachiyomi.org/help/faq/#extensions" + } + + val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") + val pkgFactory = extension.pkgFactory + return createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/README.md") + } + + fun clearCookies() { + val extension = state.value.extension ?: return + + val urls = extension.sources + .filterIsInstance() + .map { it.baseUrl } + .distinct() + + val cleared = urls.sumOf { + network.cookieManager.remove(it.toHttpUrl()) + } + + logcat { "Cleared $cleared cookies for: ${urls.joinToString()}" } + } + + fun uninstallExtension() { + val extension = state.value.extension ?: return + extensionManager.uninstallExtension(extension.pkgName) + } + + fun toggleSource(sourceId: Long) { + toggleSource.await(sourceId) + } + + fun toggleSources(enable: Boolean) { + state.value.extension?.sources + ?.map { it.id } + ?.let { toggleSource.await(it, enable) } + } + + private fun createUrl( + url: String, + pkgName: String, + pkgFactory: String?, + path: String = "", + ): String { + return if (!pkgFactory.isNullOrEmpty()) { + when (path.isEmpty()) { + true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory" + else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path + } + } else { + url + "/src/" + pkgName.replace(".", "/") + path + } + } +} + +sealed class ExtensionDetailsEvent { + object Uninstalled : ExtensionDetailsEvent() +} + +data class ExtensionDetailsState( + val extension: Extension.Installed? = null, + val sources: List = emptyList(), +) { + + val isLoading: Boolean + get() = sources.isEmpty() +}