From 761635b572f4cffa0c4979df32635bba91c0b6cb Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 29 Aug 2022 16:10:55 -0400 Subject: [PATCH] Convert extension details to full Compose --- .../browse/ExtensionDetailsScreen.kt | 81 +++++++++++-- .../details/ExtensionDetailsController.kt | 112 +----------------- .../details/ExtensionDetailsPresenter.kt | 59 ++++++++- .../tachiyomi/ui/manga/MangaPresenter.kt | 30 ++--- app/src/main/res/menu/extension_details.xml | 35 ------ 5 files changed, 143 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/res/menu/extension_details.xml diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 005284aa51..2d68ebe157 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -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) }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt index 7af4ef2251..844852ebac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt @@ -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(bundle) { - - private val network: NetworkHelper by injectLazy() +class ExtensionDetailsController( + bundle: Bundle? = null, +) : FullComposeController(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() - ?.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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt index 59d26ba1fe..b0fa224ddf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt @@ -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(), 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() + ?.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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index deeac7c667..8de5ed1014 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -99,25 +99,14 @@ class MangaPresenter( ) : BasePresenter() { private val _state: MutableStateFlow = 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? + private val processedChapters: Sequence? get() = successState?.processedChapters private val selectedPositions: Array = 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 { + private suspend fun getMangaCategoryIds(manga: DomainManga): List { return getCategories.await(manga.id) .map { it.id } } @@ -420,7 +404,7 @@ class MangaPresenter( moveMangaToCategory(categoryIds) } - fun moveMangaToCategory(categoryIds: List) { + private fun moveMangaToCategory(categoryIds: List) { 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, ) diff --git a/app/src/main/res/menu/extension_details.xml b/app/src/main/res/menu/extension_details.xml deleted file mode 100644 index 0617cdd2e8..0000000000 --- a/app/src/main/res/menu/extension_details.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - -