659 lines
26 KiB
Kotlin
659 lines
26 KiB
Kotlin
package eu.kanade.presentation.browse
|
|
|
|
import androidx.compose.animation.core.animateDpAsState
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.combinedClickable
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.FlowRow
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.RowScope
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.material3.AlertDialog
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.CircularProgressIndicator
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.LocalTextStyle
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.ProvideTextStyle
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TextButton
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
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
|
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
|
import dev.icerock.moko.resources.StringResource
|
|
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
|
import eu.kanade.presentation.browse.components.label
|
|
import eu.kanade.presentation.components.WarningBanner
|
|
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
|
import eu.kanade.tachiyomi.extension.model.Extension
|
|
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
|
|
import tachiyomi.presentation.core.components.material.padding
|
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
|
import tachiyomi.presentation.core.i18n.stringResource
|
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
|
import tachiyomi.presentation.core.theme.header
|
|
import tachiyomi.presentation.core.util.plus
|
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|
|
|
@Composable
|
|
fun ExtensionScreen(
|
|
state: ExtensionsViewModel.State,
|
|
contentPadding: PaddingValues,
|
|
searchQuery: String?,
|
|
onLongClickItem: (Extension) -> Unit,
|
|
onClickItemCancel: (Extension) -> Unit,
|
|
onOpenWebView: (Extension.Available) -> Unit,
|
|
onInstallExtension: (Extension.Available) -> Unit,
|
|
onUninstallExtension: (Extension.Installed) -> Unit,
|
|
onUpdateExtension: (Extension.Loaded) -> Unit,
|
|
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
|
onOpenExtension: (Extension.Loaded) -> Unit,
|
|
onClickUpdateAll: () -> Unit,
|
|
onRefresh: () -> Unit,
|
|
) {
|
|
val navigator = LocalNavigator.currentOrThrow
|
|
|
|
PullRefresh(
|
|
refreshing = state.isRefreshing,
|
|
onRefresh = onRefresh,
|
|
enabled = !state.isLoading,
|
|
) {
|
|
when {
|
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
|
state.isEmpty -> {
|
|
val msg = if (!searchQuery.isNullOrEmpty()) {
|
|
MR.strings.no_results_found
|
|
} else {
|
|
MR.strings.empty_screen
|
|
}
|
|
EmptyScreen(
|
|
stringRes = msg,
|
|
modifier = Modifier.padding(contentPadding),
|
|
actions = listOf(
|
|
EmptyScreenAction(
|
|
stringRes = MR.strings.extensionStores,
|
|
icon = MaterialSymbols.Rounded.Settings,
|
|
onClick = { navigator.push(ExtensionStoresScreen()) },
|
|
),
|
|
),
|
|
)
|
|
}
|
|
else -> {
|
|
ExtensionContent(
|
|
state = state,
|
|
contentPadding = contentPadding,
|
|
onLongClickItem = onLongClickItem,
|
|
onClickItemCancel = onClickItemCancel,
|
|
onOpenWebView = onOpenWebView,
|
|
onInstallExtension = onInstallExtension,
|
|
onUninstallExtension = onUninstallExtension,
|
|
onUpdateExtension = onUpdateExtension,
|
|
onTrustExtension = onTrustExtension,
|
|
onOpenExtension = onOpenExtension,
|
|
onClickUpdateAll = onClickUpdateAll,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ExtensionContent(
|
|
state: ExtensionsViewModel.State,
|
|
contentPadding: PaddingValues,
|
|
onLongClickItem: (Extension) -> Unit,
|
|
onClickItemCancel: (Extension) -> Unit,
|
|
onOpenWebView: (Extension.Available) -> Unit,
|
|
onInstallExtension: (Extension.Available) -> Unit,
|
|
onUninstallExtension: (Extension.Installed) -> Unit,
|
|
onUpdateExtension: (Extension.Loaded) -> Unit,
|
|
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
|
onOpenExtension: (Extension.Loaded) -> Unit,
|
|
onClickUpdateAll: () -> Unit,
|
|
) {
|
|
val context = LocalContext.current
|
|
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
|
|
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
|
|
|
FastScrollLazyColumn(
|
|
contentPadding = contentPadding + topSmallPaddingValues,
|
|
) {
|
|
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
|
item(key = "extension-permissions-warning") {
|
|
WarningBanner(
|
|
textRes = MR.strings.ext_permission_install_apps_warning,
|
|
modifier = Modifier.clickable {
|
|
context.launchRequestPackageInstallsPermission()
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
state.items.forEach { (header, items) ->
|
|
item(
|
|
contentType = "header",
|
|
key = "extensionHeader-${header.hashCode()}",
|
|
) {
|
|
when (header) {
|
|
is ExtensionUiModel.Header.Resource -> {
|
|
val action: @Composable RowScope.() -> Unit =
|
|
if (header.textRes == MR.strings.ext_updates_pending) {
|
|
{
|
|
Button(onClick = { onClickUpdateAll() }) {
|
|
Text(
|
|
text = stringResource(MR.strings.ext_update_all),
|
|
style = LocalTextStyle.current.copy(
|
|
color = MaterialTheme.colorScheme.onPrimary,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
{}
|
|
}
|
|
ExtensionHeader(
|
|
textRes = header.textRes,
|
|
modifier = Modifier.animateItem(),
|
|
action = action,
|
|
)
|
|
}
|
|
is ExtensionUiModel.Header.Text -> {
|
|
ExtensionHeader(
|
|
text = header.text,
|
|
modifier = Modifier.animateItem(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
items(
|
|
items = items,
|
|
contentType = { "item" },
|
|
key = { item ->
|
|
when (item.extension) {
|
|
is Extension.NotLoaded -> "extension-not-loaded-${item.hashCode()}"
|
|
is Extension.Loaded -> "extension-loaded-${item.hashCode()}"
|
|
is Extension.Available -> "extension-available-${item.hashCode()}"
|
|
}
|
|
},
|
|
) { item ->
|
|
ExtensionItem(
|
|
modifier = Modifier.animateItem(),
|
|
item = item,
|
|
onClickItem = {
|
|
when (it) {
|
|
is Extension.Available -> onInstallExtension(it)
|
|
is Extension.Loaded -> onOpenExtension(it)
|
|
is Extension.NotLoaded -> {
|
|
notLoadedState = it
|
|
}
|
|
}
|
|
},
|
|
onLongClickItem = onLongClickItem,
|
|
onClickItemSecondaryAction = {
|
|
when (it) {
|
|
is Extension.Available -> onOpenWebView(it)
|
|
is Extension.Loaded -> onOpenExtension(it)
|
|
else -> {}
|
|
}
|
|
},
|
|
onClickItemCancel = onClickItemCancel,
|
|
onClickItemAction = {
|
|
when (it) {
|
|
is Extension.Available -> onInstallExtension(it)
|
|
is Extension.Loaded -> {
|
|
if (it.hasUpdate) {
|
|
onUpdateExtension(it)
|
|
} else {
|
|
onOpenExtension(it)
|
|
}
|
|
}
|
|
is Extension.NotLoaded -> {
|
|
notLoadedState = it
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ExtensionItem(
|
|
item: ExtensionUiModel.Item,
|
|
onClickItem: (Extension) -> Unit,
|
|
onLongClickItem: (Extension) -> Unit,
|
|
onClickItemCancel: (Extension) -> Unit,
|
|
onClickItemAction: (Extension) -> Unit,
|
|
onClickItemSecondaryAction: (Extension) -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
val (extension, installStep) = item
|
|
BaseBrowseItem(
|
|
modifier = modifier
|
|
.combinedClickable(
|
|
onClick = { onClickItem(extension) },
|
|
onLongClick = { onLongClickItem(extension) },
|
|
),
|
|
onClickItem = { onClickItem(extension) },
|
|
onLongClickItem = { onLongClickItem(extension) },
|
|
icon = {
|
|
Box(
|
|
modifier = Modifier
|
|
.size(40.dp),
|
|
contentAlignment = Alignment.Center,
|
|
) {
|
|
val idle = installStep.isCompleted()
|
|
if (!idle) {
|
|
CircularProgressIndicator(
|
|
modifier = Modifier.size(40.dp),
|
|
strokeWidth = 2.dp,
|
|
)
|
|
}
|
|
|
|
val padding by animateDpAsState(
|
|
targetValue = if (idle) 0.dp else 8.dp,
|
|
label = "iconPadding",
|
|
)
|
|
ExtensionIcon(
|
|
extension = extension,
|
|
modifier = Modifier
|
|
.matchParentSize()
|
|
.padding(padding),
|
|
)
|
|
}
|
|
},
|
|
action = {
|
|
ExtensionItemActions(
|
|
extension = extension,
|
|
installStep = installStep,
|
|
onClickItemCancel = onClickItemCancel,
|
|
onClickItemAction = onClickItemAction,
|
|
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
|
)
|
|
},
|
|
) {
|
|
ExtensionItemContent(
|
|
extension = extension,
|
|
installStep = installStep,
|
|
modifier = Modifier.weight(1f),
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ExtensionItemContent(
|
|
extension: Extension,
|
|
installStep: InstallStep,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
Column(
|
|
modifier = modifier.padding(start = MaterialTheme.padding.medium),
|
|
) {
|
|
Text(
|
|
text = extension.name,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
)
|
|
// Won't look good but it's not like we can ellipsize overflowing content
|
|
FlowRow(
|
|
modifier = Modifier.secondaryItemAlpha(),
|
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
|
) {
|
|
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
|
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
|
|
if (extension is Extension.Loaded && extension.lang.isNotEmpty()) {
|
|
hasAlreadyShownAnElement = true
|
|
Text(
|
|
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
|
)
|
|
}
|
|
|
|
if (extension.versionName.isNotEmpty()) {
|
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
|
hasAlreadyShownAnElement = true
|
|
Text(
|
|
text = extension.versionName,
|
|
)
|
|
}
|
|
|
|
val warnings = listOfNotNull(
|
|
when {
|
|
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
|
|
},
|
|
extension.contentWarning.label?.let { it.title to it.color },
|
|
)
|
|
warnings.forEach { (label, color) ->
|
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
|
hasAlreadyShownAnElement = true
|
|
Text(
|
|
text = stringResource(label).uppercase(),
|
|
color = color,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
)
|
|
}
|
|
|
|
if (extension is Extension.Loaded && !extension.isShared) {
|
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
|
Text(
|
|
text = stringResource(MR.strings.ext_installer_private),
|
|
)
|
|
}
|
|
|
|
if (!installStep.isCompleted()) {
|
|
DotSeparatorNoSpaceText()
|
|
Text(
|
|
text = when (installStep) {
|
|
InstallStep.Pending -> stringResource(MR.strings.ext_pending)
|
|
InstallStep.Downloading -> stringResource(MR.strings.ext_downloading)
|
|
InstallStep.Installing -> stringResource(MR.strings.ext_installing)
|
|
else -> error("Must not show non-install process text")
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ExtensionItemActions(
|
|
extension: Extension,
|
|
installStep: InstallStep,
|
|
modifier: Modifier = Modifier,
|
|
onClickItemCancel: (Extension) -> Unit = {},
|
|
onClickItemAction: (Extension) -> Unit = {},
|
|
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
|
) {
|
|
val isIdle = installStep.isCompleted()
|
|
|
|
Row(
|
|
modifier = modifier,
|
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
|
) {
|
|
when {
|
|
!isIdle -> {
|
|
IconButton(onClick = { onClickItemCancel(extension) }) {
|
|
Icon(
|
|
imageVector = MaterialSymbols.Rounded.Close,
|
|
contentDescription = stringResource(MR.strings.action_cancel),
|
|
)
|
|
}
|
|
}
|
|
installStep == InstallStep.Error -> {
|
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
Icon(
|
|
imageVector = MaterialSymbols.Rounded.Refresh,
|
|
contentDescription = stringResource(MR.strings.action_retry),
|
|
)
|
|
}
|
|
}
|
|
installStep == InstallStep.Idle -> {
|
|
when (extension) {
|
|
is Extension.Loaded -> {
|
|
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
|
Icon(
|
|
imageVector = MaterialSymbols.Rounded.Settings,
|
|
contentDescription = stringResource(MR.strings.action_settings),
|
|
)
|
|
}
|
|
|
|
if (extension.hasUpdate) {
|
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
Icon(
|
|
imageVector = MaterialSymbols.Rounded.Download,
|
|
contentDescription = stringResource(MR.strings.ext_update),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
is Extension.NotLoaded -> {
|
|
val isUntrusted = extension.reason is Extension.NotLoaded.Reason.Untrusted
|
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
Icon(
|
|
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)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
is Extension.Available -> {
|
|
if (extension.sources.isNotEmpty()) {
|
|
IconButton(
|
|
onClick = { onClickItemSecondaryAction(extension) },
|
|
) {
|
|
Icon(
|
|
imageVector = MaterialSymbols.Rounded.Public,
|
|
contentDescription = stringResource(MR.strings.action_open_in_web_view),
|
|
)
|
|
}
|
|
}
|
|
|
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
Icon(
|
|
imageVector = MaterialSymbols.Rounded.Download,
|
|
contentDescription = stringResource(MR.strings.ext_install),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ExtensionHeader(
|
|
textRes: StringResource,
|
|
modifier: Modifier = Modifier,
|
|
action: @Composable RowScope.() -> Unit = {},
|
|
) {
|
|
ExtensionHeader(
|
|
text = stringResource(textRes),
|
|
modifier = modifier,
|
|
action = action,
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun ExtensionHeader(
|
|
text: String,
|
|
modifier: Modifier = Modifier,
|
|
action: @Composable RowScope.() -> Unit = {},
|
|
) {
|
|
Row(
|
|
modifier = modifier.padding(horizontal = MaterialTheme.padding.medium),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
) {
|
|
Text(
|
|
text = text,
|
|
modifier = Modifier
|
|
.padding(vertical = 8.dp)
|
|
.weight(1f),
|
|
style = MaterialTheme.typography.header,
|
|
)
|
|
action()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
onClickDismiss: () -> Unit,
|
|
onDismissRequest: () -> Unit,
|
|
) {
|
|
AlertDialog(
|
|
title = {
|
|
Text(text = stringResource(MR.strings.untrusted_extension))
|
|
},
|
|
text = {
|
|
Text(text = stringResource(MR.strings.untrusted_extension_message))
|
|
},
|
|
confirmButton = {
|
|
TextButton(onClick = onClickConfirm) {
|
|
Text(text = stringResource(MR.strings.ext_trust))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = onClickDismiss) {
|
|
Text(text = stringResource(MR.strings.ext_uninstall))
|
|
}
|
|
},
|
|
onDismissRequest = onDismissRequest,
|
|
)
|
|
}
|