mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +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