mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Use Voyager on Extension Details screen (#8576)
This commit is contained in:
		@@ -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,
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -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<ExtensionSourceItem>,
 | 
			
		||||
    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
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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<ExtensionSourceItem>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<ExtensionSourceItem> by mutableStateOf(emptyList())
 | 
			
		||||
}
 | 
			
		||||
@@ -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<ExtensionDetailsPresenter>(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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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<ExtensionDetailsController>(), 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<HttpSource>()
 | 
			
		||||
            ?.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"
 | 
			
		||||
@@ -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()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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>(ExtensionDetailsState()) {
 | 
			
		||||
 | 
			
		||||
    private val _events: Channel<ExtensionDetailsEvent> = Channel()
 | 
			
		||||
    val events: Flow<ExtensionDetailsEvent> = _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<HttpSource>()
 | 
			
		||||
            .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<ExtensionSourceItem> = emptyList(),
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    val isLoading: Boolean
 | 
			
		||||
        get() = sources.isEmpty()
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user