Expose installed extensions that failed to load (#3953)
This commit is contained in:
@@ -31,6 +31,7 @@ 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.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
@@ -48,14 +49,17 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.Close
|
||||
import mihon.icons.materialsymbols.rounded.Download
|
||||
import mihon.icons.materialsymbols.rounded.Info
|
||||
import mihon.icons.materialsymbols.rounded.Public
|
||||
import mihon.icons.materialsymbols.rounded.Refresh
|
||||
import mihon.icons.materialsymbols.rounded.Settings
|
||||
import mihon.icons.materialsymbols.rounded.VerifiedUser
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
@@ -78,10 +82,10 @@ fun ExtensionScreen(
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Loaded) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
@@ -139,14 +143,14 @@ private fun ExtensionContent(
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Loaded) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||
|
||||
FastScrollLazyColumn(
|
||||
@@ -205,8 +209,8 @@ private fun ExtensionContent(
|
||||
contentType = { "item" },
|
||||
key = { item ->
|
||||
when (item.extension) {
|
||||
is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}"
|
||||
is Extension.Installed -> "extension-installed-${item.hashCode()}"
|
||||
is Extension.NotLoaded -> "extension-not-loaded-${item.hashCode()}"
|
||||
is Extension.Loaded -> "extension-loaded-${item.hashCode()}"
|
||||
is Extension.Available -> "extension-available-${item.hashCode()}"
|
||||
}
|
||||
},
|
||||
@@ -217,9 +221,9 @@ private fun ExtensionContent(
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
is Extension.Loaded -> onOpenExtension(it)
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -227,7 +231,7 @@ private fun ExtensionContent(
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onOpenWebView(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Loaded -> onOpenExtension(it)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
@@ -235,15 +239,15 @@ private fun ExtensionContent(
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> {
|
||||
is Extension.Loaded -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
onOpenExtension(it)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -251,20 +255,30 @@ private fun ExtensionContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trustState != null) {
|
||||
ExtensionTrustDialog(
|
||||
onClickConfirm = {
|
||||
onTrustExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onClickDismiss = {
|
||||
onUninstallExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onDismissRequest = {
|
||||
trustState = null
|
||||
},
|
||||
)
|
||||
notLoadedState?.let { extension ->
|
||||
val dismiss = { notLoadedState = null }
|
||||
if (extension.reason is Extension.NotLoaded.Reason.Untrusted) {
|
||||
ExtensionTrustDialog(
|
||||
onClickConfirm = {
|
||||
onTrustExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onClickDismiss = {
|
||||
onUninstallExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onDismissRequest = dismiss,
|
||||
)
|
||||
} else {
|
||||
ExtensionNotLoadedDialog(
|
||||
reason = extension.reason,
|
||||
onClickUninstall = {
|
||||
onUninstallExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onDismissRequest = dismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +367,7 @@ private fun ExtensionItemContent(
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
|
||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||
if (extension is Extension.Loaded && extension.lang.isNotEmpty()) {
|
||||
hasAlreadyShownAnElement = true
|
||||
Text(
|
||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||
@@ -370,9 +384,9 @@ private fun ExtensionItemContent(
|
||||
|
||||
val warnings = listOfNotNull(
|
||||
when {
|
||||
extension is Extension.Untrusted ->
|
||||
MR.strings.ext_untrusted to MaterialTheme.colorScheme.error
|
||||
extension is Extension.Installed && extension.isObsolete ->
|
||||
extension is Extension.NotLoaded ->
|
||||
extension.reason.labelRes?.let { it to MaterialTheme.colorScheme.error }
|
||||
extension is Extension.Loaded && extension.isObsolete ->
|
||||
MR.strings.ext_obsolete to MaterialTheme.colorScheme.error
|
||||
else -> null
|
||||
},
|
||||
@@ -389,7 +403,7 @@ private fun ExtensionItemContent(
|
||||
)
|
||||
}
|
||||
|
||||
if (extension is Extension.Installed && !extension.isShared) {
|
||||
if (extension is Extension.Loaded && !extension.isShared) {
|
||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||
Text(
|
||||
text = stringResource(MR.strings.ext_installer_private),
|
||||
@@ -446,7 +460,7 @@ private fun ExtensionItemActions(
|
||||
}
|
||||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
is Extension.Loaded -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.Settings,
|
||||
@@ -463,11 +477,20 @@ private fun ExtensionItemActions(
|
||||
}
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
is Extension.NotLoaded -> {
|
||||
val isUntrusted = extension.reason is Extension.NotLoaded.Reason.Untrusted
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.VerifiedUser,
|
||||
contentDescription = stringResource(MR.strings.ext_trust),
|
||||
imageVector = if (isUntrusted) {
|
||||
MaterialSymbols.Rounded.VerifiedUser
|
||||
} else {
|
||||
MaterialSymbols.Rounded.Info
|
||||
},
|
||||
contentDescription = if (isUntrusted) {
|
||||
stringResource(MR.strings.ext_trust)
|
||||
} else {
|
||||
stringResource(MR.strings.ext_not_loaded)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -530,6 +553,83 @@ private fun ExtensionHeader(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the reasons the user can act on are worth naming in the row; the rest all mean "broken" to
|
||||
* them and are spelled out in [ExtensionNotLoadedDialog] instead.
|
||||
*/
|
||||
private val Extension.NotLoaded.Reason.labelRes: StringResource?
|
||||
get() = when (this) {
|
||||
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.ext_untrusted
|
||||
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered
|
||||
// The section header already says these aren't loaded; the dialog says why
|
||||
Extension.NotLoaded.Reason.Unsigned,
|
||||
Extension.NotLoaded.Reason.UnsupportedLibVersion,
|
||||
Extension.NotLoaded.Reason.Malformed,
|
||||
is Extension.NotLoaded.Reason.Failed,
|
||||
-> null
|
||||
}
|
||||
|
||||
private val Extension.NotLoaded.Reason.descriptionRes: StringResource
|
||||
get() = when (this) {
|
||||
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.untrusted_extension_message
|
||||
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered_message
|
||||
Extension.NotLoaded.Reason.Unsigned -> MR.strings.ext_unsigned_message
|
||||
Extension.NotLoaded.Reason.UnsupportedLibVersion -> MR.strings.ext_unsupported_message
|
||||
Extension.NotLoaded.Reason.Malformed -> MR.strings.ext_malformed_message
|
||||
is Extension.NotLoaded.Reason.Failed -> MR.strings.ext_load_failed_message
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionNotLoadedDialog(
|
||||
reason: Extension.NotLoaded.Reason,
|
||||
onClickUninstall: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.ext_not_loaded_dialog))
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
|
||||
Text(text = stringResource(reason.descriptionRes))
|
||||
|
||||
if (reason is Extension.NotLoaded.Reason.Failed) {
|
||||
Text(
|
||||
text = reason.message,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
TextButton(
|
||||
onClick = {
|
||||
context.copyToClipboard(
|
||||
label = context.stringResource(MR.strings.ext_copy_stacktrace),
|
||||
content = reason.stackTrace,
|
||||
)
|
||||
},
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.ext_copy_stacktrace))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onClickUninstall) {
|
||||
Text(text = stringResource(MR.strings.ext_uninstall))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionTrustDialog(
|
||||
onClickConfirm: () -> Unit,
|
||||
|
||||
Reference in New Issue
Block a user