mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-23 13:23:28 +03:00
Use Voyager on Extension Details screen (#8576)
This commit is contained in:
parent
b7fa25777d
commit
f1b85ff39d
7 changed files with 325 additions and 264 deletions
|
@ -3,7 +3,6 @@ package eu.kanade.domain.extension.interactor
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||||
import eu.kanade.presentation.components.Divider
|
import eu.kanade.presentation.components.Divider
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
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.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsPresenter
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionDetailsScreen(
|
fun ExtensionDetailsScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
presenter: ExtensionDetailsPresenter,
|
state: ExtensionDetailsState,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
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(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
|
@ -80,19 +83,19 @@ fun ExtensionDetailsScreen(
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = buildList {
|
actions = buildList {
|
||||||
if (presenter.extension?.isUnofficial == false) {
|
if (state.extension?.isUnofficial == false) {
|
||||||
add(
|
add(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(R.string.whats_new),
|
title = stringResource(R.string.whats_new),
|
||||||
icon = Icons.Outlined.History,
|
icon = Icons.Outlined.History,
|
||||||
onClick = { uriHandler.openUri(presenter.getChangelogUrl()) },
|
onClick = onClickWhatsNew,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(R.string.action_faq_and_guides),
|
title = stringResource(R.string.action_faq_and_guides),
|
||||||
icon = Icons.Outlined.HelpOutline,
|
icon = Icons.Outlined.HelpOutline,
|
||||||
onClick = { uriHandler.openUri(presenter.getReadmeUrl()) },
|
onClick = onClickReadme,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -100,15 +103,15 @@ fun ExtensionDetailsScreen(
|
||||||
listOf(
|
listOf(
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.action_enable_all),
|
title = stringResource(R.string.action_enable_all),
|
||||||
onClick = { presenter.toggleSources(true) },
|
onClick = onClickEnableAll,
|
||||||
),
|
),
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.action_disable_all),
|
title = stringResource(R.string.action_disable_all),
|
||||||
onClick = { presenter.toggleSources(false) },
|
onClick = onClickDisableAll,
|
||||||
),
|
),
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.pref_clear_cookies),
|
title = stringResource(R.string.pref_clear_cookies),
|
||||||
onClick = { presenter.clearCookies() },
|
onClick = onClickClearCookies,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -119,25 +122,36 @@ fun ExtensionDetailsScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { 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
|
@Composable
|
||||||
private fun ExtensionDetails(
|
private fun ExtensionDetails(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
presenter: ExtensionDetailsPresenter,
|
extension: Extension.Installed,
|
||||||
|
sources: List<ExtensionSourceItem>,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
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 context = LocalContext.current
|
||||||
val extension = presenter.extension
|
|
||||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
|
@ -157,7 +171,7 @@ private fun ExtensionDetails(
|
||||||
item {
|
item {
|
||||||
DetailsHeader(
|
DetailsHeader(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
onClickUninstall = { presenter.uninstallExtension() },
|
onClickUninstall = onClickUninstall,
|
||||||
onClickAppInfo = {
|
onClickAppInfo = {
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
data = Uri.fromParts("package", extension.pkgName, null)
|
data = Uri.fromParts("package", extension.pkgName, null)
|
||||||
|
@ -171,14 +185,14 @@ private fun ExtensionDetails(
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = presenter.sources,
|
items = sources,
|
||||||
key = { it.source.id },
|
key = { it.source.id },
|
||||||
) { source ->
|
) { source ->
|
||||||
SourceSwitchPreference(
|
SourceSwitchPreference(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
source = source,
|
source = source,
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
onClickSourcePreferences = onClickSourcePreferences,
|
||||||
onClickSource = { presenter.toggleSource(it) },
|
onClickSource = onClickSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,8 +203,6 @@ private fun ExtensionDetails(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -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
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.presentation.browse.ExtensionDetailsScreen
|
import eu.kanade.presentation.browse.ExtensionDetailsScreen
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
private const val PKGNAME_KEY = "pkg_name"
|
||||||
class ExtensionDetailsController(
|
|
||||||
bundle: Bundle? = null,
|
|
||||||
) : FullComposeController<ExtensionDetailsPresenter>(bundle) {
|
|
||||||
|
|
||||||
constructor(pkgName: String) : this(
|
class ExtensionDetailsController : BasicFullComposeController {
|
||||||
bundleOf(PKGNAME_KEY to pkgName),
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
ExtensionDetailsScreen(
|
Navigator(screen = ExtensionDetailsScreen(pkgName = pkgName))
|
||||||
navigateUp = router::popCurrentController,
|
|
||||||
presenter = presenter,
|
|
||||||
onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onExtensionUninstalled() {
|
|
||||||
router.popCurrentController()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
}
|
Loading…
Reference in a new issue