Convert extension details to full Compose

This commit is contained in:
arkon 2022-08-29 16:10:55 -04:00
parent 488d8ab8cf
commit 761635b572
5 changed files with 143 additions and 174 deletions

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@ -20,9 +21,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@ -41,22 +45,25 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.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.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.PreferenceRow
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource
@ -66,11 +73,68 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
fun ExtensionDetailsScreen(
nestedScrollInterop: NestedScrollConnection,
navigateUp: () -> Unit,
presenter: ExtensionDetailsPresenter,
onClickSourcePreferences: (sourceId: Long) -> Unit,
) {
val uriHandler = LocalUriHandler.current
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(R.string.label_extension_info),
navigateUp = navigateUp,
actions = {
AppBarActions(
actions = buildList {
if (presenter.extension?.isUnofficial == false) {
add(
AppBar.Action(
title = stringResource(R.string.whats_new),
icon = Icons.Outlined.History,
onClick = { uriHandler.openUri(presenter.getChangelogUrl()) },
),
)
add(
AppBar.Action(
title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline,
onClick = { uriHandler.openUri(presenter.getReadmeUrl()) },
),
)
}
addAll(
listOf(
AppBar.OverflowAction(
title = stringResource(R.string.action_enable_all),
onClick = { presenter.toggleSources(true) },
),
AppBar.OverflowAction(
title = stringResource(R.string.action_disable_all),
onClick = { presenter.toggleSources(false) },
),
AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies),
onClick = { presenter.clearCookies() },
),
),
)
},
)
},
)
},
) { paddingValues ->
ExtensionDetails(paddingValues, presenter, onClickSourcePreferences)
}
}
@Composable
private fun ExtensionDetails(
paddingValues: PaddingValues,
presenter: ExtensionDetailsPresenter,
onClickUninstall: () -> Unit,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) {
when {
presenter.isLoading -> LoadingScreen()
@ -81,8 +145,7 @@ fun ExtensionDetailsScreen(
var showNsfwWarning by remember { mutableStateOf(false) }
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
contentPadding = paddingValues + WindowInsets.navigationBars.asPaddingValues(),
) {
when {
extension.isUnofficial ->
@ -98,7 +161,7 @@ fun ExtensionDetailsScreen(
item {
DetailsHeader(
extension = extension,
onClickUninstall = onClickUninstall,
onClickUninstall = { presenter.uninstallExtension() },
onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", extension.pkgName, null)
@ -119,7 +182,7 @@ fun ExtensionDetailsScreen(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,
onClickSource = { presenter.toggleSource(it) },
)
}
}

View file

@ -2,135 +2,35 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.core.os.bundleOf
import eu.kanade.presentation.browse.ExtensionDetailsScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.util.system.logcat
import okhttp3.HttpUrl.Companion.toHttpUrl
import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) :
ComposeController<ExtensionDetailsPresenter>(bundle) {
private val network: NetworkHelper by injectLazy()
class ExtensionDetailsController(
bundle: Bundle? = null,
) : FullComposeController<ExtensionDetailsPresenter>(bundle) {
constructor(pkgName: String) : this(
bundleOf(PKGNAME_KEY to pkgName),
)
init {
setHasOptionsMenu(true)
}
override fun getTitle() = resources?.getString(R.string.label_extension_info)
override fun createPresenter() = ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
@Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
override fun ComposeContent() {
ExtensionDetailsScreen(
nestedScrollInterop = nestedScrollInterop,
navigateUp = router::popCurrentController,
presenter = presenter,
onClickUninstall = { presenter.uninstallExtension() },
onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
onClickSource = { presenter.toggleSource(it) },
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.extension_details, menu)
presenter.extension?.let { extension ->
menu.findItem(R.id.action_history).isVisible = !extension.isUnofficial
menu.findItem(R.id.action_faq_and_guides).isVisible = !extension.isUnofficial
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_history -> openChangelog()
R.id.action_faq_and_guides -> openReadme()
R.id.action_enable_all -> toggleAllSources(true)
R.id.action_disable_all -> toggleAllSources(false)
R.id.action_clear_cookies -> clearCookies()
}
return super.onOptionsItemSelected(item)
}
fun onExtensionUninstalled() {
router.popCurrentController()
}
private fun toggleAllSources(enable: Boolean) {
presenter.toggleSources(enable)
}
private fun openChangelog() {
val extension = presenter.extension!!
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
val pkgFactory = extension.pkgFactory
if (extension.hasChangelog) {
val url = createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/CHANGELOG.md")
openInBrowser(url)
return
}
// Falling back on GitHub commit history because there is no explicit changelog in extension
val url = createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory)
openInBrowser(url)
}
private fun openReadme() {
val extension = presenter.extension!!
if (!extension.hasReadme) {
openInBrowser("https://tachiyomi.org/help/faq/#extensions")
return
}
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
val pkgFactory = extension.pkgFactory
val url = createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/README.md")
openInBrowser(url)
return
}
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
}
}
private fun clearCookies() {
val urls = presenter.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()}" }
}
}
private const val PKGNAME_KEY = "pkg_name"
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

@ -7,14 +7,18 @@ 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
@ -24,6 +28,7 @@ class ExtensionDetailsPresenter(
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 {
@ -32,7 +37,7 @@ class ExtensionDetailsPresenter(
presenterScope.launchIO {
extensionManager.getInstalledExtensionsFlow()
.map { it.firstOrNull { it.pkgName == pkgName } }
.map { it.firstOrNull { pkg-> pkg.pkgName == pkgName } }
.collectLatest { extension ->
// If extension is null it's most likely uninstalled
if (extension == null) {
@ -65,6 +70,44 @@ class ExtensionDetailsPresenter(
}
}
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)
@ -77,6 +120,17 @@ class ExtensionDetailsPresenter(
fun toggleSources(enable: Boolean) {
extension?.sources?.forEach { toggleSource.await(it.id, 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(
@ -84,3 +138,6 @@ data class ExtensionSourceItem(
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

@ -99,25 +99,14 @@ class MangaPresenter(
) : BasePresenter<MangaController>() {
private val _state: MutableStateFlow<MangaScreenState> = MutableStateFlow(MangaScreenState.Loading)
val state = _state.asStateFlow()
private val successState: MangaScreenState.Success?
get() = state.value as? MangaScreenState.Success
/**
* Subscription to update the manga from the source.
*/
private var fetchMangaJob: Job? = null
/**
* Subscription to retrieve the new list of chapters from the source.
*/
private var fetchChaptersJob: Job? = null
/**
* Subscription to observe download status changes.
*/
private var observeDownloadsStatusJob: Job? = null
private var observeDownloadsPageJob: Job? = null
@ -138,7 +127,7 @@ class MangaPresenter(
val isFavoritedManga: Boolean
get() = manga?.favorite ?: false
val processedChapters: Sequence<ChapterItem>?
private val processedChapters: Sequence<ChapterItem>?
get() = successState?.processedChapters
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
@ -164,8 +153,6 @@ class MangaPresenter(
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Manga info - start
presenterScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId)
@ -221,15 +208,11 @@ class MangaPresenter(
}
preferences.incognitoMode()
.asHotFlow { incognito ->
incognitoMode = incognito
}
.asHotFlow { incognitoMode = it }
.launchIn(presenterScope)
preferences.downloadedOnly()
.asHotFlow { downloadedOnly ->
downloadedOnlyMode = downloadedOnly
}
.asHotFlow { downloadedOnlyMode = it }
.launchIn(presenterScope)
}
@ -239,6 +222,7 @@ class MangaPresenter(
}
// Manga info - start
/**
* Fetch manga information from source.
*/
@ -395,7 +379,7 @@ class MangaPresenter(
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
suspend fun getMangaCategoryIds(manga: DomainManga): List<Long> {
private suspend fun getMangaCategoryIds(manga: DomainManga): List<Long> {
return getCategories.await(manga.id)
.map { it.id }
}
@ -420,7 +404,7 @@ class MangaPresenter(
moveMangaToCategory(categoryIds)
}
fun moveMangaToCategory(categoryIds: List<Long>) {
private fun moveMangaToCategory(categoryIds: List<Long>) {
presenterScope.launchIO {
setMangaCategories.await(mangaId, categoryIds)
}
@ -951,7 +935,7 @@ class MangaPresenter(
.lastOrNull()
?.chapterNumber?.toDouble() ?: -1.0
if (latestLocalReadChapterNumber >= track.lastChapterRead) {
if (latestLocalReadChapterNumber > track.lastChapterRead) {
val updatedTrack = track.copy(
lastChapterRead = latestLocalReadChapterNumber,
)

View file

@ -1,35 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_history"
android:icon="@drawable/ic_history_24dp"
android:title="@string/whats_new"
android:visible="false"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_faq_and_guides"
android:icon="@drawable/ic_help_24dp"
android:title="@string/action_faq_and_guides"
android:visible="false"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_enable_all"
android:title="@string/action_enable_all"
app:showAsAction="never" />
<item
android:id="@+id/action_disable_all"
android:title="@string/action_disable_all"
app:showAsAction="never" />
<item
android:id="@+id/action_clear_cookies"
android:title="@string/pref_clear_cookies"
app:showAsAction="never" />
</menu>