Use Voyager on Extension Details screen (#8576)

This commit is contained in:
Andreas
2022-11-20 20:36:03 +01:00
committed by GitHub
parent b7fa25777d
commit f1b85ff39d
7 changed files with 325 additions and 264 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()
}
}
}
}
}

View File

@@ -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()
}