mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Show an extension's store and install failures in the extension list (#3955)
This commit is contained in:
@@ -32,8 +32,9 @@ class GetExtensionsByType(
|
|||||||
|
|
||||||
val available = _available
|
val available = _available
|
||||||
.filter { extension ->
|
.filter { extension ->
|
||||||
_loaded.none { it.pkgName == extension.pkgName } &&
|
(_loaded + _notLoaded).none {
|
||||||
_notLoaded.none { it.pkgName == extension.pkgName } &&
|
it.pkgName == extension.pkgName && extension.store.signingKey in it.signatures
|
||||||
|
} &&
|
||||||
extension.contentWarning in enabledContentWarnings
|
extension.contentWarning in enabledContentWarnings
|
||||||
}
|
}
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
@@ -47,7 +48,10 @@ class GetExtensionsByType(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sortedWith(byName)
|
.sortedWith(
|
||||||
|
compareBy<Extension.Available, String>(String.CASE_INSENSITIVE_ORDER) { it.name }
|
||||||
|
.thenBy { it.store.signingKey },
|
||||||
|
)
|
||||||
|
|
||||||
Extensions(
|
Extensions(
|
||||||
updates = updates,
|
updates = updates,
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
@@ -32,6 +32,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -43,6 +44,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.browse.components.ExtensionPill
|
||||||
import eu.kanade.presentation.browse.components.label
|
import eu.kanade.presentation.browse.components.label
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
@@ -56,7 +58,6 @@ import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsViewModel
|
|||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import mihon.icons.materialsymbols.MaterialSymbols
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
import mihon.icons.materialsymbols.automirroredrounded.OpenInNew
|
|
||||||
import mihon.icons.materialsymbols.rounded.Settings
|
import mihon.icons.materialsymbols.rounded.Settings
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
@@ -77,15 +78,6 @@ fun ExtensionDetailsScreen(
|
|||||||
onClickIncognito: (Boolean) -> Unit,
|
onClickIncognito: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val url = remember(state.extension) {
|
|
||||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
|
||||||
regex.find(state.extension.store?.indexUrl.orEmpty())
|
|
||||||
?.let {
|
|
||||||
val (user, repo) = it.destructured
|
|
||||||
"https://github.com/$user/$repo"
|
|
||||||
}
|
|
||||||
?: state.extension.store?.indexUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
@@ -94,35 +86,20 @@ fun ExtensionDetailsScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = buildList {
|
actions = listOf(
|
||||||
if (url != null) {
|
AppBar.OverflowAction(
|
||||||
add(
|
title = stringResource(MR.strings.action_enable_all),
|
||||||
AppBar.Action(
|
onClick = onClickEnableAll,
|
||||||
title = stringResource(MR.strings.action_open_repo),
|
),
|
||||||
icon = MaterialSymbols.AutoMirroredRounded.OpenInNew,
|
AppBar.OverflowAction(
|
||||||
onClick = {
|
title = stringResource(MR.strings.action_disable_all),
|
||||||
uriHandler.openUri(url)
|
onClick = onClickDisableAll,
|
||||||
},
|
),
|
||||||
),
|
AppBar.OverflowAction(
|
||||||
)
|
title = stringResource(MR.strings.pref_clear_cookies),
|
||||||
}
|
onClick = onClickClearCookies,
|
||||||
addAll(
|
),
|
||||||
listOf(
|
),
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_enable_all),
|
|
||||||
onClick = onClickEnableAll,
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_disable_all),
|
|
||||||
onClick = onClickDisableAll,
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
|
||||||
onClick = onClickClearCookies,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
@@ -132,6 +109,11 @@ fun ExtensionDetailsScreen(
|
|||||||
ExtensionDetails(
|
ExtensionDetails(
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
extension = state.extension,
|
extension = state.extension,
|
||||||
|
onClickStore = state.extension.store
|
||||||
|
?.contact
|
||||||
|
?.website
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { website -> { uriHandler.openUri(website) } },
|
||||||
sources = state.sources,
|
sources = state.sources,
|
||||||
incognitoMode = state.isIncognito,
|
incognitoMode = state.isIncognito,
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
onClickSourcePreferences = onClickSourcePreferences,
|
||||||
@@ -146,6 +128,7 @@ fun ExtensionDetailsScreen(
|
|||||||
private fun ExtensionDetails(
|
private fun ExtensionDetails(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
extension: Extension.Loaded,
|
extension: Extension.Loaded,
|
||||||
|
onClickStore: (() -> Unit)?,
|
||||||
sources: List<ExtensionSourceItem>,
|
sources: List<ExtensionSourceItem>,
|
||||||
incognitoMode: Boolean,
|
incognitoMode: Boolean,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
@@ -178,6 +161,7 @@ private fun ExtensionDetails(
|
|||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}.takeIf { extension.isShared },
|
}.takeIf { extension.isShared },
|
||||||
|
onClickStore = onClickStore,
|
||||||
onClickContentWarning = {
|
onClickContentWarning = {
|
||||||
showContentWarning = true
|
showContentWarning = true
|
||||||
},
|
},
|
||||||
@@ -210,8 +194,9 @@ private fun ExtensionDetails(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DetailsHeader(
|
private fun DetailsHeader(
|
||||||
extension: Extension,
|
extension: Extension.Installed,
|
||||||
extIncognitoMode: Boolean,
|
extIncognitoMode: Boolean,
|
||||||
|
onClickStore: (() -> Unit)?,
|
||||||
onClickContentWarning: () -> Unit,
|
onClickContentWarning: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickAppInfo: (() -> Unit)?,
|
onClickAppInfo: (() -> Unit)?,
|
||||||
@@ -271,12 +256,35 @@ private fun DetailsHeader(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = strippedPkgName,
|
text = extension.pkgName,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
extension.store?.let { store ->
|
||||||
|
Text(
|
||||||
|
text = store.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.clickable(enabled = onClickStore != null) { onClickStore?.invoke() }
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.extraSmall,
|
||||||
|
vertical = MaterialTheme.padding.extraSmall / 2,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((extension as? Extension.Loaded)?.isShared == false) {
|
||||||
|
ExtensionPill(
|
||||||
|
text = stringResource(MR.strings.ext_installer_private),
|
||||||
|
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -330,14 +338,11 @@ private fun DetailsHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (onClickAppInfo != null) {
|
if (onClickAppInfo != null) {
|
||||||
Button(
|
FilledTonalButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
onClick = onClickAppInfo,
|
onClick = onClickAppInfo,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(stringResource(MR.strings.ext_app_info))
|
||||||
text = stringResource(MR.strings.ext_app_info),
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,11 +436,10 @@ private fun SourceSwitchPreference(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (source.source is ConfigurableSource) {
|
if (source.source is ConfigurableSource) {
|
||||||
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
FilledTonalIconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = MaterialSymbols.Rounded.Settings,
|
imageVector = MaterialSymbols.Rounded.Settings,
|
||||||
contentDescription = stringResource(MR.strings.label_settings),
|
contentDescription = stringResource(MR.strings.label_settings),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,17 +10,22 @@ import androidx.compose.foundation.layout.FlowRow
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
import androidx.compose.material3.SplitButtonDefaults
|
||||||
|
import androidx.compose.material3.SplitButtonLayout
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -30,18 +35,22 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.browse.components.ExtensionPill
|
||||||
import eu.kanade.presentation.browse.components.label
|
import eu.kanade.presentation.browse.components.label
|
||||||
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
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.more.settings.screen.browse.ExtensionStoresScreen
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
@@ -51,11 +60,12 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
|
|||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
import mihon.icons.materialsymbols.MaterialSymbols
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
import mihon.icons.materialsymbols.rounded.Close
|
import mihon.icons.materialsymbols.rounded.Close
|
||||||
import mihon.icons.materialsymbols.rounded.Download
|
import mihon.icons.materialsymbols.rounded.Download
|
||||||
|
import mihon.icons.materialsymbols.rounded.ExpandMore
|
||||||
import mihon.icons.materialsymbols.rounded.Info
|
import mihon.icons.materialsymbols.rounded.Info
|
||||||
import mihon.icons.materialsymbols.rounded.Public
|
|
||||||
import mihon.icons.materialsymbols.rounded.Refresh
|
import mihon.icons.materialsymbols.rounded.Refresh
|
||||||
import mihon.icons.materialsymbols.rounded.Settings
|
import mihon.icons.materialsymbols.rounded.Settings
|
||||||
import mihon.icons.materialsymbols.rounded.VerifiedUser
|
import mihon.icons.materialsymbols.rounded.VerifiedUser
|
||||||
@@ -71,7 +81,6 @@ import tachiyomi.presentation.core.screens.EmptyScreenAction
|
|||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import tachiyomi.presentation.core.theme.header
|
import tachiyomi.presentation.core.theme.header
|
||||||
import tachiyomi.presentation.core.util.plus
|
import tachiyomi.presentation.core.util.plus
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionScreen(
|
fun ExtensionScreen(
|
||||||
@@ -151,6 +160,7 @@ private fun ExtensionContent(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
|
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
|
||||||
|
var installErrorState by remember { mutableStateOf<InstallStep.Error?>(null) }
|
||||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
@@ -238,6 +248,7 @@ private fun ExtensionContent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
|
onClickItemError = { installErrorState = it },
|
||||||
onClickItemAction = {
|
onClickItemAction = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Extension.Available -> onInstallExtension(it)
|
is Extension.Available -> onInstallExtension(it)
|
||||||
@@ -286,6 +297,55 @@ private fun ExtensionContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
installErrorState?.let { error ->
|
||||||
|
ExtensionInstallErrorDialog(
|
||||||
|
error = error,
|
||||||
|
onDismissRequest = { installErrorState = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExtensionInstallErrorDialog(
|
||||||
|
error: InstallStep.Error,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.ext_install_error))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
|
||||||
|
Text(
|
||||||
|
text = error.message,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
error.stackTrace?.let { stackTrace ->
|
||||||
|
val context = LocalContext.current
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
context.copyToClipboard(
|
||||||
|
label = context.stringResource(MR.strings.ext_copy_stacktrace),
|
||||||
|
content = stackTrace,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.ext_copy_stacktrace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -294,50 +354,30 @@ private fun ExtensionItem(
|
|||||||
onClickItem: (Extension) -> Unit,
|
onClickItem: (Extension) -> Unit,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
|
onClickItemError: (InstallStep.Error) -> Unit,
|
||||||
onClickItemAction: (Extension) -> Unit,
|
onClickItemAction: (Extension) -> Unit,
|
||||||
onClickItemSecondaryAction: (Extension) -> Unit,
|
onClickItemSecondaryAction: (Extension) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val (extension, installStep) = item
|
val (extension, installStep) = item
|
||||||
|
val store = when (extension) {
|
||||||
|
is Extension.Available -> extension.store
|
||||||
|
is Extension.Installed -> extension.store
|
||||||
|
}
|
||||||
|
|
||||||
BaseBrowseItem(
|
BaseBrowseItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.combinedClickable(
|
|
||||||
onClick = { onClickItem(extension) },
|
|
||||||
onLongClick = { onLongClickItem(extension) },
|
|
||||||
),
|
|
||||||
onClickItem = { onClickItem(extension) },
|
onClickItem = { onClickItem(extension) },
|
||||||
onLongClickItem = { onLongClickItem(extension) },
|
onLongClickItem = { onLongClickItem(extension) },
|
||||||
icon = {
|
icon = {
|
||||||
Box(
|
ExtensionItemIcon(extension = extension, installStep = installStep)
|
||||||
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 = {
|
action = {
|
||||||
ExtensionItemActions(
|
ExtensionItemActions(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
installStep = installStep,
|
installStep = installStep,
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
|
onClickItemError = onClickItemError,
|
||||||
onClickItemAction = onClickItemAction,
|
onClickItemAction = onClickItemAction,
|
||||||
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||||
)
|
)
|
||||||
@@ -346,6 +386,7 @@ private fun ExtensionItem(
|
|||||||
ExtensionItemContent(
|
ExtensionItemContent(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
installStep = installStep,
|
installStep = installStep,
|
||||||
|
store = store,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -355,6 +396,7 @@ private fun ExtensionItem(
|
|||||||
private fun ExtensionItemContent(
|
private fun ExtensionItemContent(
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
installStep: InstallStep,
|
installStep: InstallStep,
|
||||||
|
store: ExtensionStore?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -366,68 +408,106 @@ private fun ExtensionItemContent(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
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 (store != null) {
|
||||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
Text(
|
||||||
hasAlreadyShownAnElement = true
|
text = store.name,
|
||||||
Text(
|
maxLines = 1,
|
||||||
text = extension.versionName,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
style = MaterialTheme.typography.bodySmall,
|
||||||
}
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val warnings = listOfNotNull(
|
ExtensionItemMetadata(extension = extension, installStep = installStep)
|
||||||
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) {
|
@Composable
|
||||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
private fun ExtensionItemIcon(
|
||||||
Text(
|
extension: Extension,
|
||||||
text = stringResource(MR.strings.ext_installer_private),
|
installStep: InstallStep,
|
||||||
)
|
modifier: Modifier = Modifier,
|
||||||
}
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.size(48.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val idle = installStep.isCompleted()
|
||||||
|
if (!idle) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!installStep.isCompleted()) {
|
val padding by animateDpAsState(
|
||||||
DotSeparatorNoSpaceText()
|
targetValue = if (idle) 0.dp else 8.dp,
|
||||||
Text(
|
label = "iconPadding",
|
||||||
text = when (installStep) {
|
)
|
||||||
InstallStep.Pending -> stringResource(MR.strings.ext_pending)
|
ExtensionIcon(
|
||||||
InstallStep.Downloading -> stringResource(MR.strings.ext_downloading)
|
extension = extension,
|
||||||
InstallStep.Installing -> stringResource(MR.strings.ext_installing)
|
modifier = Modifier
|
||||||
else -> error("Must not show non-install process text")
|
.matchParentSize()
|
||||||
},
|
.padding(padding),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain text for what an extension simply is, pills for the few things worth noticing about it.
|
||||||
|
* Won't look good when it wraps, but it's not like overflowing content can be ellipsized.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ExtensionItemMetadata(
|
||||||
|
extension: Extension,
|
||||||
|
installStep: InstallStep,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall / 2),
|
||||||
|
itemVerticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val facts = listOfNotNull(
|
||||||
|
(extension as? Extension.Loaded)
|
||||||
|
?.lang
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { LocaleHelper.getSourceDisplayName(it, LocalContext.current) },
|
||||||
|
extension.versionName.takeIf { it.isNotEmpty() },
|
||||||
|
)
|
||||||
|
if (facts.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = facts.joinToString(" \u2022 "),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notable = listOfNotNull(
|
||||||
|
when (installStep) {
|
||||||
|
InstallStep.Pending -> MR.strings.ext_pending
|
||||||
|
InstallStep.Downloading -> MR.strings.ext_downloading
|
||||||
|
InstallStep.Installing -> MR.strings.ext_installing
|
||||||
|
else -> null
|
||||||
|
}?.let { it to MaterialTheme.colorScheme.primary },
|
||||||
|
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 },
|
||||||
|
(extension as? Extension.Loaded)
|
||||||
|
?.takeIf { !it.isShared }
|
||||||
|
?.let { MR.strings.ext_installer_private to MaterialTheme.colorScheme.onSurfaceVariant },
|
||||||
|
)
|
||||||
|
notable.forEach { (label, color) ->
|
||||||
|
ExtensionPill(text = stringResource(label), color = color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,17 +518,30 @@ private fun ExtensionItemActions(
|
|||||||
installStep: InstallStep,
|
installStep: InstallStep,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickItemCancel: (Extension) -> Unit = {},
|
onClickItemCancel: (Extension) -> Unit = {},
|
||||||
|
onClickItemError: (InstallStep.Error) -> Unit = {},
|
||||||
onClickItemAction: (Extension) -> Unit = {},
|
onClickItemAction: (Extension) -> Unit = {},
|
||||||
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val isIdle = installStep.isCompleted()
|
val isUntrusted = (extension as? Extension.NotLoaded)?.reason is Extension.NotLoaded.Reason.Untrusted
|
||||||
|
val secondaryAction = when (extension) {
|
||||||
|
is Extension.Available -> extension.sources.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { stringResource(MR.strings.action_open_in_web_view) }
|
||||||
|
is Extension.Loaded -> stringResource(MR.strings.action_settings)
|
||||||
|
is Extension.NotLoaded -> if (isUntrusted) {
|
||||||
|
stringResource(MR.strings.ext_trust)
|
||||||
|
} else {
|
||||||
|
stringResource(MR.strings.ext_not_loaded_details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?.let { it to { onClickItemSecondaryAction(extension) } }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
!isIdle -> {
|
!installStep.isCompleted() -> {
|
||||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = MaterialSymbols.Rounded.Close,
|
imageVector = MaterialSymbols.Rounded.Close,
|
||||||
@@ -456,84 +549,123 @@ private fun ExtensionItemActions(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
installStep == InstallStep.Error -> {
|
installStep is InstallStep.Error -> {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
ExtensionSplitButton(
|
||||||
|
icon = MaterialSymbols.Rounded.Refresh,
|
||||||
|
contentDescription = stringResource(MR.strings.action_retry),
|
||||||
|
onClick = { onClickItemAction(extension) },
|
||||||
|
menuItems = listOfNotNull(
|
||||||
|
stringResource(MR.strings.ext_install_error_details) to { onClickItemError(installStep) },
|
||||||
|
secondaryAction,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extension is Extension.Available -> {
|
||||||
|
ExtensionSplitButton(
|
||||||
|
icon = MaterialSymbols.Rounded.Download,
|
||||||
|
contentDescription = stringResource(MR.strings.ext_install),
|
||||||
|
onClick = { onClickItemAction(extension) },
|
||||||
|
menuItems = listOfNotNull(secondaryAction),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extension is Extension.Installed && extension.hasUpdate -> {
|
||||||
|
ExtensionSplitButton(
|
||||||
|
icon = MaterialSymbols.Rounded.Download,
|
||||||
|
contentDescription = stringResource(MR.strings.ext_update),
|
||||||
|
onClick = { onClickItemAction(extension) },
|
||||||
|
menuItems = listOfNotNull(secondaryAction),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extension is Extension.Loaded -> {
|
||||||
|
FilledTonalIconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = MaterialSymbols.Rounded.Refresh,
|
imageVector = MaterialSymbols.Rounded.Settings,
|
||||||
contentDescription = stringResource(MR.strings.action_retry),
|
contentDescription = stringResource(MR.strings.action_settings),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
installStep == InstallStep.Idle -> {
|
extension is Extension.NotLoaded -> {
|
||||||
when (extension) {
|
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||||
is Extension.Loaded -> {
|
Icon(
|
||||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
imageVector = if (isUntrusted) {
|
||||||
Icon(
|
MaterialSymbols.Rounded.VerifiedUser
|
||||||
imageVector = MaterialSymbols.Rounded.Settings,
|
} else {
|
||||||
contentDescription = stringResource(MR.strings.action_settings),
|
MaterialSymbols.Rounded.Info
|
||||||
)
|
},
|
||||||
}
|
contentDescription = secondaryAction?.first,
|
||||||
|
)
|
||||||
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 = { onClickItemSecondaryAction(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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = MaterialSymbols.Rounded.Download,
|
|
||||||
contentDescription = stringResource(MR.strings.ext_update),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 ExtensionSplitButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
menuItems: List<Pair<String, () -> Unit>>,
|
||||||
|
) {
|
||||||
|
if (menuItems.isEmpty()) {
|
||||||
|
FilledTonalIconButton(onClick = onClick) {
|
||||||
|
Icon(imageVector = icon, contentDescription = contentDescription)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val size = SplitButtonDefaults.ExtraSmallContainerHeight
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
SplitButtonLayout(
|
||||||
|
leadingButton = {
|
||||||
|
SplitButtonDefaults.TonalLeadingButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.height(size),
|
||||||
|
shapes = SplitButtonDefaults.leadingButtonShapesFor(size),
|
||||||
|
contentPadding = SplitButtonDefaults.leadingButtonContentPaddingFor(size),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = Modifier.size(SplitButtonDefaults.leadingButtonIconSizeFor(size)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingButton = {
|
||||||
|
Box {
|
||||||
|
SplitButtonDefaults.TonalTrailingButton(
|
||||||
|
checked = expanded,
|
||||||
|
onCheckedChange = { expanded = it },
|
||||||
|
modifier = Modifier.height(size),
|
||||||
|
shapes = SplitButtonDefaults.trailingButtonShapesFor(size),
|
||||||
|
contentPadding = SplitButtonDefaults.trailingButtonContentPaddingFor(size),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MaterialSymbols.Rounded.ExpandMore,
|
||||||
|
contentDescription = stringResource(MR.strings.action_menu),
|
||||||
|
modifier = Modifier.size(SplitButtonDefaults.trailingButtonIconSizeFor(size)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
offset = DpOffset.Zero,
|
||||||
|
) {
|
||||||
|
menuItems.forEach { (text, action) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = text) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
action()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionHeader(
|
private fun ExtensionHeader(
|
||||||
textRes: StringResource,
|
textRes: StringResource,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -16,6 +17,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.presentation.browse.components.SourceIcon
|
import eu.kanade.presentation.browse.components.SourceIcon
|
||||||
@@ -156,7 +158,7 @@ private fun MigrateSourceItem(
|
|||||||
showLanguageInContent = source.lang != "",
|
showLanguageInContent = source.lang != "",
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
icon = { SourceIcon(source = source) },
|
icon = { SourceIcon(source = source, modifier = Modifier.size(48.dp)) },
|
||||||
action = {
|
action = {
|
||||||
BadgeGroup {
|
BadgeGroup {
|
||||||
Badge(text = "$count")
|
Badge(text = "$count")
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package eu.kanade.presentation.browse.components
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
@@ -39,7 +41,7 @@ fun BaseSourceItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
|
private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
|
||||||
SourceIcon(source = source)
|
SourceIcon(source = source, modifier = Modifier.size(48.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ExtensionPill(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = modifier
|
||||||
|
.background(color = color.copy(alpha = PILL_BACKGROUND_ALPHA), shape = MaterialTheme.shapes.small)
|
||||||
|
.padding(horizontal = MaterialTheme.padding.small, vertical = 2.dp),
|
||||||
|
color = color,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PILL_BACKGROUND_ALPHA = 0.12f
|
||||||
@@ -68,13 +68,26 @@ class ExtensionManager(
|
|||||||
|
|
||||||
private val iconMap = mutableMapOf<String, Drawable>()
|
private val iconMap = mutableMapOf<String, Drawable>()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var stores = emptyList<ExtensionStore>()
|
||||||
|
|
||||||
private val loadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Loaded>())
|
private val loadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Loaded>())
|
||||||
val loadedExtensionsFlow = loadedExtensionMapFlow.mapExtensionsWhenInitialized()
|
val loadedExtensionsFlow = loadedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||||
|
|
||||||
// Every store's listing, since more than one store can list the same extension
|
// Every store's listing, since more than one store can list the same extension
|
||||||
private val availableExtensionListFlow = MutableStateFlow(emptyList<Extension.Available>())
|
private val availableExtensionListFlow = MutableStateFlow(emptyList<Extension.Available>())
|
||||||
|
|
||||||
|
// Stores sharing a signing key serve the same apks, so only the newest of their listings is shown. Stores
|
||||||
|
// with different keys offer different apks, each installable, so each keeps its own.
|
||||||
val availableExtensionsFlow = availableExtensionListFlow
|
val availableExtensionsFlow = availableExtensionListFlow
|
||||||
.map { extensions -> extensions.associateBy { it.pkgName }.values.toList() }
|
.map { extensions ->
|
||||||
|
extensions
|
||||||
|
.groupBy { it.pkgName to it.store.signingKey }
|
||||||
|
.values
|
||||||
|
.map { listings ->
|
||||||
|
listings.maxWith(compareBy<Extension.Available> { it.versionCode }.thenBy { it.libVersion })
|
||||||
|
}
|
||||||
|
}
|
||||||
.stateIn(scope, SharingStarted.Lazily, emptyList())
|
.stateIn(scope, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
private val notLoadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.NotLoaded>())
|
private val notLoadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.NotLoaded>())
|
||||||
@@ -378,23 +391,38 @@ class ExtensionManager(
|
|||||||
extension.copy(
|
extension.copy(
|
||||||
hasUpdate = extension.findUpdate(available) != null,
|
hasUpdate = extension.findUpdate(available) != null,
|
||||||
isObsolete = listing == null,
|
isObsolete = listing == null,
|
||||||
|
store = if (stores.isEmpty()) extension.store else extension.pickStore(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
notLoadedExtensionMapFlow.value = notLoadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
notLoadedExtensionMapFlow.value = notLoadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
||||||
extension.copy(hasUpdate = extension.findUpdate(available) != null)
|
extension.copy(
|
||||||
|
hasUpdate = extension.findUpdate(available) != null,
|
||||||
|
store = if (stores.isEmpty()) extension.store else extension.pickStore(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatePendingUpdatesCount()
|
updatePendingUpdatesCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignStores(stores: List<ExtensionStore>) {
|
/**
|
||||||
fun Extension.Installed.signingStore() = stores.firstOrNull { it.signingKey in signatures }
|
* Several stores can share a signing key, so one that lists the extension names where it comes from better
|
||||||
|
* than whichever of them was added first.
|
||||||
|
*/
|
||||||
|
private fun Extension.Installed.pickStore(): ExtensionStore? {
|
||||||
|
val signingStores = stores.filter { it.signingKey in signatures }
|
||||||
|
val listedBy = availableExtensionListFlow.value
|
||||||
|
.filter { it.pkgName == pkgName }
|
||||||
|
.mapTo(HashSet()) { it.store.indexUrl }
|
||||||
|
return signingStores.firstOrNull { it.indexUrl in listedBy } ?: signingStores.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignStores(stores: List<ExtensionStore>) {
|
||||||
|
this.stores = stores
|
||||||
loadedExtensionMapFlow.update { extensions ->
|
loadedExtensionMapFlow.update { extensions ->
|
||||||
extensions.mapValues { (_, extension) -> extension.copy(store = extension.signingStore()) }
|
extensions.mapValues { (_, extension) -> extension.copy(store = extension.pickStore()) }
|
||||||
}
|
}
|
||||||
notLoadedExtensionMapFlow.update { extensions ->
|
notLoadedExtensionMapFlow.update { extensions ->
|
||||||
extensions.mapValues { (_, extension) -> extension.copy(store = extension.signingStore()) }
|
extensions.mapValues { (_, extension) -> extension.copy(store = extension.pickStore()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,12 @@ abstract class Installer(private val service: Service) {
|
|||||||
@CallSuper
|
@CallSuper
|
||||||
open fun onDestroy() {
|
open fun onDestroy() {
|
||||||
cancelListeners -= cancelListener
|
cancelListeners -= cancelListener
|
||||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
queue.forEach {
|
||||||
|
extensionManager.updateInstallStep(
|
||||||
|
it.downloadId,
|
||||||
|
InstallStep.Error("The installer stopped before it got to this one"),
|
||||||
|
)
|
||||||
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
waitingInstall.store(null)
|
waitingInstall.store(null)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -24,7 +24,8 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
|
|
||||||
private val packageActionReceiver = object : BroadcastReceiver() {
|
private val packageActionReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
|
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
|
||||||
|
when (status) {
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
|
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
|
||||||
?.run {
|
?.run {
|
||||||
@@ -43,7 +44,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
}
|
}
|
||||||
if (userAction == null) {
|
if (userAction == null) {
|
||||||
logcat(LogPriority.ERROR) { "Fatal error for $intent" }
|
logcat(LogPriority.ERROR) { "Fatal error for $intent" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error("The system installer asked for an action it didn't provide"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
@@ -53,7 +54,12 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
continueQueue(InstallStep.Idle)
|
continueQueue(InstallStep.Idle)
|
||||||
}
|
}
|
||||||
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
|
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
|
||||||
else -> continueQueue(InstallStep.Error)
|
else -> continueQueue(
|
||||||
|
InstallStep.Error(
|
||||||
|
intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||||
|
?: "The system installer failed ($status)",
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +105,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
activeSession?.let { (_, sessionId) ->
|
activeSession?.let { (_, sessionId) ->
|
||||||
packageInstaller.abandonSession(sessionId)
|
packageInstaller.abandonSession(sessionId)
|
||||||
}
|
}
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error.from(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
continueQueue(InstallStep.Installed)
|
continueQueue(InstallStep.Installed)
|
||||||
} else {
|
} else {
|
||||||
logcat(LogPriority.ERROR) { "Failed to install extension $packageName: $message" }
|
logcat(LogPriority.ERROR) { "Failed to install extension $packageName: $message" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error(message ?: "Shizuku failed to install it ($status)"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
service.contentResolver.delete(entry.uri, null, null)
|
service.contentResolver.delete(entry.uri, null, null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error.from(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
package eu.kanade.tachiyomi.extension.model
|
||||||
|
|
||||||
enum class InstallStep {
|
sealed interface InstallStep {
|
||||||
Idle,
|
data object Idle : InstallStep
|
||||||
Pending,
|
data object Pending : InstallStep
|
||||||
Downloading,
|
data object Downloading : InstallStep
|
||||||
Installing,
|
data object Installing : InstallStep
|
||||||
Installed,
|
data object Installed : InstallStep
|
||||||
Error,
|
|
||||||
;
|
/**
|
||||||
|
* [message] says what went wrong; [stackTrace] is there when an exception did.
|
||||||
|
*/
|
||||||
|
data class Error(val message: String, val stackTrace: String? = null) : InstallStep {
|
||||||
|
companion object {
|
||||||
|
fun from(e: Throwable): Error {
|
||||||
|
val root = generateSequence(e) { it.cause }.last()
|
||||||
|
return Error(
|
||||||
|
message = root.message ?: root::class.simpleName.orEmpty(),
|
||||||
|
stackTrace = e.stackTraceToString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun isCompleted(): Boolean {
|
fun isCompleted(): Boolean {
|
||||||
return this == Installed || this == Error || this == Idle
|
return this == Installed || this is Error || this == Idle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ExtensionInstallActivity : Activity() {
|
|||||||
val newStep = when (resultCode) {
|
val newStep = when (resultCode) {
|
||||||
RESULT_OK -> InstallStep.Installed
|
RESULT_OK -> InstallStep.Installed
|
||||||
RESULT_CANCELED -> InstallStep.Idle
|
RESULT_CANCELED -> InstallStep.Idle
|
||||||
else -> InstallStep.Error
|
else -> InstallStep.Error("The installer reported a failure")
|
||||||
}
|
}
|
||||||
extensionManager.updateInstallStep(downloadId, newStep)
|
extensionManager.updateInstallStep(downloadId, newStep)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class ExtensionInstaller(
|
|||||||
val downloadId = extension.pkgName.hashCode().toLong()
|
val downloadId = extension.pkgName.hashCode().toLong()
|
||||||
cancelInstall(extension.pkgName)
|
cancelInstall(extension.pkgName)
|
||||||
|
|
||||||
val step = MutableStateFlow(InstallStep.Pending)
|
val step = MutableStateFlow<InstallStep>(InstallStep.Pending)
|
||||||
activeSteps[downloadId] = step
|
activeSteps[downloadId] = step
|
||||||
|
|
||||||
val job = scope.launch {
|
val job = scope.launch {
|
||||||
@@ -94,7 +94,7 @@ class ExtensionInstaller(
|
|||||||
// Canceled
|
// Canceled
|
||||||
} else {
|
} else {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
step.value = InstallStep.Error
|
step.value = InstallStep.Error.from(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,14 +155,11 @@ class ExtensionInstaller(
|
|||||||
|
|
||||||
private fun installApkPrivately(downloadId: Long, tempFile: File, packageInfo: PackageInfo) {
|
private fun installApkPrivately(downloadId: Long, tempFile: File, packageInfo: PackageInfo) {
|
||||||
try {
|
try {
|
||||||
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile, packageInfo)) {
|
ExtensionLoader.installPrivateExtensionFile(context, tempFile, packageInfo)
|
||||||
updateInstallStep(downloadId, InstallStep.Installed)
|
updateInstallStep(downloadId, InstallStep.Installed)
|
||||||
} else {
|
|
||||||
updateInstallStep(downloadId, InstallStep.Error)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
logcat(LogPriority.ERROR, e) { "Failed to install extension privately" }
|
||||||
updateInstallStep(downloadId, InstallStep.Error)
|
updateInstallStep(downloadId, InstallStep.Error.from(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
|
|||||||
@@ -60,44 +60,35 @@ internal object ExtensionLoader {
|
|||||||
|
|
||||||
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
||||||
|
|
||||||
fun installPrivateExtensionFile(context: Context, file: File, extension: PackageInfo): Boolean {
|
fun installPrivateExtensionFile(context: Context, file: File, extension: PackageInfo) {
|
||||||
if (!isPackageAnExtension(extension)) return false
|
check(isPackageAnExtension(extension)) { "${extension.packageName} isn't an extension" }
|
||||||
val currentExtension = getExtensionPackageInfoFromPkgName(context, extension.packageName)
|
val currentExtension = getExtensionPackageInfoFromPkgName(context, extension.packageName)
|
||||||
|
|
||||||
if (currentExtension != null) {
|
if (currentExtension != null) {
|
||||||
if (PackageInfoCompat.getLongVersionCode(extension) <
|
check(
|
||||||
PackageInfoCompat.getLongVersionCode(currentExtension)
|
PackageInfoCompat.getLongVersionCode(extension) >=
|
||||||
) {
|
PackageInfoCompat.getLongVersionCode(currentExtension),
|
||||||
logcat(LogPriority.ERROR) { "Installed extension version is higher. Downgrading is not allowed." }
|
) { "The installed version is newer, and downgrading isn't allowed" }
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val extensionSignatures = getSignatures(extension)
|
val extensionSignatures = getSignatures(extension)
|
||||||
if (extensionSignatures.isNullOrEmpty()) {
|
check(!extensionSignatures.isNullOrEmpty()) { "The extension isn't signed" }
|
||||||
logcat(LogPriority.ERROR) { "Extension to be installed is not signed." }
|
check(extensionSignatures.containsAll(getSignatures(currentExtension)!!)) {
|
||||||
return false
|
"It isn't signed with the same key as the installed extension"
|
||||||
}
|
|
||||||
|
|
||||||
if (!extensionSignatures.containsAll(getSignatures(currentExtension)!!)) {
|
|
||||||
logcat(LogPriority.ERROR) { "Installed extension signature is not matched." }
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
|
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
|
||||||
return try {
|
try {
|
||||||
target.delete()
|
target.delete()
|
||||||
file.copyAndSetReadOnlyTo(target, overwrite = true)
|
file.copyAndSetReadOnlyTo(target, overwrite = true)
|
||||||
if (currentExtension != null) {
|
|
||||||
ExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
|
|
||||||
} else {
|
|
||||||
ExtensionInstallReceiver.notifyAdded(context, extension.packageName)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to copy extension file." }
|
|
||||||
target.delete()
|
target.delete()
|
||||||
false
|
throw e
|
||||||
|
}
|
||||||
|
if (currentExtension != null) {
|
||||||
|
ExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
|
||||||
|
} else {
|
||||||
|
ExtensionInstallReceiver.notifyAdded(context, extension.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -343,6 +343,9 @@
|
|||||||
<string name="ext_filtered">Filtered</string>
|
<string name="ext_filtered">Filtered</string>
|
||||||
<string name="ext_not_loaded">Not loaded</string>
|
<string name="ext_not_loaded">Not loaded</string>
|
||||||
<string name="ext_not_loaded_dialog">Couldn\'t load</string>
|
<string name="ext_not_loaded_dialog">Couldn\'t load</string>
|
||||||
|
<string name="ext_install_error">Couldn\'t install</string>
|
||||||
|
<string name="ext_install_error_details">Why it failed</string>
|
||||||
|
<string name="ext_not_loaded_details">Why it isn\'t loaded</string>
|
||||||
<string name="ext_copy_stacktrace">Copy stack trace</string>
|
<string name="ext_copy_stacktrace">Copy stack trace</string>
|
||||||
<string name="ext_filtered_message">This extension\'s content warning isn\'t one you chose to load, so its sources aren\'t available.</string>
|
<string name="ext_filtered_message">This extension\'s content warning isn\'t one you chose to load, so its sources aren\'t available.</string>
|
||||||
<string name="ext_unsigned_message">This extension isn\'t signed, so there\'s no signature to check it against.</string>
|
<string name="ext_unsigned_message">This extension isn\'t signed, so there\'s no signature to check it against.</string>
|
||||||
@@ -379,7 +382,6 @@
|
|||||||
<string name="downloading_with_progress">Downloading… (%d%%)</string>
|
<string name="downloading_with_progress">Downloading… (%d%%)</string>
|
||||||
|
|
||||||
<!-- Extension repos -->
|
<!-- Extension repos -->
|
||||||
<string name="action_open_repo">Open source repo</string>
|
|
||||||
|
|
||||||
<!-- Extension store -->
|
<!-- Extension store -->
|
||||||
<string name="extensionStores">Extension stores</string>
|
<string name="extensionStores">Extension stores</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user