From 8972882a3467314e24a22c16c222717d5477c79f Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:12:54 +0600 Subject: [PATCH] Use plain icon buttons for extension actions --- .../browse/ExtensionDetailsScreen.kt | 9 +- .../presentation/browse/ExtensionsScreen.kt | 235 +++--------------- .../extension/installer/Installer.kt | 8 +- .../installer/PackageInstallerInstaller.kt | 24 +- .../extension/installer/ShizukuInstaller.kt | 8 +- .../tachiyomi/extension/model/InstallStep.kt | 18 +- .../util/ExtensionInstallActivity.kt | 7 +- .../extension/util/ExtensionInstaller.kt | 8 +- .../moko-resources/base/strings.xml | 2 - 9 files changed, 78 insertions(+), 241 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 36f830d2c..993361003 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -15,10 +15,10 @@ 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.FilledTonalButton -import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Button import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Switch @@ -336,7 +336,7 @@ private fun DetailsHeader( } if (onClickAppInfo != null) { - FilledTonalButton( + Button( modifier = Modifier.weight(1f), onClick = onClickAppInfo, ) { @@ -434,10 +434,11 @@ private fun SourceSwitchPreference( verticalAlignment = Alignment.CenterVertically, ) { if (source.source is ConfigurableSource) { - FilledTonalIconButton(onClick = { onClickSourcePreferences(source.source.id) }) { + IconButton(onClick = { onClickSourcePreferences(source.source.id) }) { Icon( imageVector = MaterialSymbols.Rounded.Settings, contentDescription = stringResource(MR.strings.label_settings), + tint = MaterialTheme.colorScheme.onSurface, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index b7e523be6..d5d7960c7 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -10,7 +10,6 @@ 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.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.items @@ -18,14 +17,10 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SplitButtonDefaults -import androidx.compose.material3.SplitButtonLayout import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -36,11 +31,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -49,7 +42,6 @@ import eu.kanade.presentation.browse.components.BaseBrowseItem 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.components.DropdownMenu import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState @@ -65,7 +57,7 @@ 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.KeyboardArrowDown +import mihon.icons.materialsymbols.rounded.Public import mihon.icons.materialsymbols.rounded.Refresh import mihon.icons.materialsymbols.rounded.Settings import mihon.icons.materialsymbols.rounded.VerifiedUser @@ -160,7 +152,6 @@ private fun ExtensionContent( ) { val context = LocalContext.current var notLoadedState by remember { mutableStateOf(null) } - var installErrorState by remember { mutableStateOf(null) } val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true) FastScrollLazyColumn( @@ -248,7 +239,6 @@ private fun ExtensionContent( } }, onClickItemCancel = onClickItemCancel, - onClickItemError = { installErrorState = it }, onClickItemAction = { when (it) { is Extension.Available -> onInstallExtension(it) @@ -297,55 +287,6 @@ 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 @@ -354,7 +295,6 @@ private fun ExtensionItem( onClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, - onClickItemError: (InstallStep.Error) -> Unit, onClickItemAction: (Extension) -> Unit, onClickItemSecondaryAction: (Extension) -> Unit, modifier: Modifier = Modifier, @@ -377,7 +317,6 @@ private fun ExtensionItem( extension = extension, installStep = installStep, onClickItemCancel = onClickItemCancel, - onClickItemError = onClickItemError, onClickItemAction = onClickItemAction, onClickItemSecondaryAction = onClickItemSecondaryAction, ) @@ -518,152 +457,56 @@ private fun ExtensionItemActions( installStep: InstallStep, modifier: Modifier = Modifier, onClickItemCancel: (Extension) -> Unit = {}, - onClickItemError: (InstallStep.Error) -> Unit = {}, onClickItemAction: (Extension) -> Unit = {}, onClickItemSecondaryAction: (Extension) -> Unit = {}, ) { - 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( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalAlignment = Alignment.CenterVertically, ) { - when { - !installStep.isCompleted() -> { - IconButton(onClick = { onClickItemCancel(extension) }) { - Icon( - imageVector = MaterialSymbols.Rounded.Close, - contentDescription = stringResource(MR.strings.action_cancel), - ) - } - } - installStep is InstallStep.Error -> { - 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( - imageVector = MaterialSymbols.Rounded.Settings, - contentDescription = stringResource(MR.strings.action_settings), - ) - } - } - extension is Extension.NotLoaded -> { - IconButton(onClick = { onClickItemSecondaryAction(extension) }) { - Icon( - imageVector = if (isUntrusted) { - MaterialSymbols.Rounded.VerifiedUser - } else { - MaterialSymbols.Rounded.Info - }, - contentDescription = secondaryAction?.first, - ) - } - } - } - } -} - -@Composable -private fun ExtensionSplitButton( - icon: ImageVector, - contentDescription: String, - onClick: () -> Unit, - menuItems: List 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), - ) { + if (!installStep.isCompleted()) { + IconButton(onClick = { onClickItemCancel(extension) }) { Icon( - imageVector = icon, - contentDescription = contentDescription, - modifier = Modifier.size(SplitButtonDefaults.leadingButtonIconSizeFor(size)), + imageVector = MaterialSymbols.Rounded.Close, + contentDescription = stringResource(MR.strings.action_cancel), ) } - }, - 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.KeyboardArrowDown, - 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() - }, - ) - } - } + return@Row + } + + val secondaryAction = when (extension) { + is Extension.Available -> { + (MaterialSymbols.Rounded.Public to MR.strings.action_open_in_web_view) + .takeIf { extension.sources.isNotEmpty() } } - }, - ) + is Extension.Loaded -> MaterialSymbols.Rounded.Settings to MR.strings.action_settings + is Extension.NotLoaded -> if (extension.reason is Extension.NotLoaded.Reason.Untrusted) { + MaterialSymbols.Rounded.VerifiedUser to MR.strings.ext_trust + } else { + MaterialSymbols.Rounded.Info to MR.strings.ext_not_loaded_details + } + } + secondaryAction?.let { (icon, label) -> + IconButton(onClick = { onClickItemSecondaryAction(extension) }) { + Icon(imageVector = icon, contentDescription = stringResource(label)) + } + } + + val action = when { + installStep == InstallStep.Error -> MaterialSymbols.Rounded.Refresh to MR.strings.action_retry + extension is Extension.Available -> MaterialSymbols.Rounded.Download to MR.strings.ext_install + extension is Extension.Installed && extension.hasUpdate -> { + MaterialSymbols.Rounded.Download to MR.strings.ext_update + } + else -> null + } + action?.let { (icon, label) -> + IconButton(onClick = { onClickItemAction(extension) }) { + Icon(imageVector = icon, contentDescription = stringResource(label)) + } + } + } } @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt index 84194bb26..3900806b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt @@ -5,7 +5,9 @@ import android.net.Uri import androidx.annotation.CallSuper import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.InstallStep +import logcat.LogPriority import mihon.app.di.appGraph +import tachiyomi.core.common.util.system.logcat import java.util.Collections import java.util.concurrent.CopyOnWriteArraySet import kotlin.concurrent.atomics.AtomicReference @@ -107,10 +109,8 @@ abstract class Installer(private val service: Service) { open fun onDestroy() { cancelListeners -= cancelListener queue.forEach { - extensionManager.updateInstallStep( - it.downloadId, - InstallStep.Error("The installer stopped before it got to this one"), - ) + logcat(LogPriority.INFO) { "The installer stopped before installing ${it.downloadId}" } + extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) } queue.clear() waitingInstall.store(null) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index 6c3b81238..0125f2365 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -43,8 +43,10 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic .sanitizeByFiltering(this) } if (userAction == null) { - logcat(LogPriority.ERROR) { "Fatal error for $intent" } - continueQueue(InstallStep.Error("The system installer asked for an action it didn't provide")) + logcat(LogPriority.INFO) { + "The system installer asked for an action it didn't provide: $intent" + } + continueQueue(InstallStep.Error) return } userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -54,12 +56,14 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic continueQueue(InstallStep.Idle) } PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed) - else -> continueQueue( - InstallStep.Error( - intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - ?: "The system installer failed ($status)", - ), - ) + else -> { + logcat(LogPriority.INFO) { + "The system installer failed ($status): ${intent.getStringExtra( + PackageInstaller.EXTRA_STATUS_MESSAGE, + )}" + } + continueQueue(InstallStep.Error) + } } } } @@ -101,11 +105,11 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic session.commit(intentSender) } } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } + logcat(LogPriority.INFO, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } activeSession?.let { (_, sessionId) -> packageInstaller.abandonSession(sessionId) } - continueQueue(InstallStep.Error.from(e)) + continueQueue(InstallStep.Error) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt index 001195749..2e989337a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt @@ -71,8 +71,8 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { if (status == PackageInstaller.STATUS_SUCCESS) { continueQueue(InstallStep.Installed) } else { - logcat(LogPriority.ERROR) { "Failed to install extension $packageName: $message" } - continueQueue(InstallStep.Error(message ?: "Shizuku failed to install it ($status)")) + logcat(LogPriority.INFO) { "Shizuku failed to install $packageName ($status): $message" } + continueQueue(InstallStep.Error) } } } @@ -123,8 +123,8 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { } service.contentResolver.delete(entry.uri, null, null) } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } - continueQueue(InstallStep.Error.from(e)) + logcat(LogPriority.INFO, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } + continueQueue(InstallStep.Error) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt index 413604f38..ea82004c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt @@ -6,23 +6,9 @@ sealed interface InstallStep { data object Downloading : InstallStep data object Installing : InstallStep data object Installed : InstallStep - - /** - * [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(), - ) - } - } - } + data object Error : InstallStep fun isCompleted(): Boolean { - return this == Installed || this is Error || this == Idle + return this == Installed || this == Error || this == Idle } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt index 93cf8dcd1..be24edca9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt @@ -8,7 +8,9 @@ import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller import eu.kanade.tachiyomi.util.system.toast +import logcat.LogPriority import mihon.app.di.appGraph +import tachiyomi.core.common.util.system.logcat import kotlin.time.Duration.Companion.seconds /** @@ -77,7 +79,10 @@ class ExtensionInstallActivity : Activity() { val newStep = when (resultCode) { RESULT_OK -> InstallStep.Installed RESULT_CANCELED -> InstallStep.Idle - else -> InstallStep.Error("The installer reported a failure") + else -> { + logcat(LogPriority.INFO) { "The installer reported a failure ($resultCode)" } + InstallStep.Error + } } extensionManager.updateInstallStep(downloadId, newStep) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 6ab08725f..453fd2223 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -93,8 +93,8 @@ class ExtensionInstaller( if (e is InterruptedException) { // Canceled } else { - logcat(LogPriority.ERROR, e) - step.value = InstallStep.Error.from(e) + logcat(LogPriority.INFO, e) + step.value = InstallStep.Error } } } @@ -158,8 +158,8 @@ class ExtensionInstaller( ExtensionLoader.installPrivateExtensionFile(context, tempFile, packageInfo) updateInstallStep(downloadId, InstallStep.Installed) } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to install extension privately" } - updateInstallStep(downloadId, InstallStep.Error.from(e)) + logcat(LogPriority.INFO, e) { "Failed to install extension privately" } + updateInstallStep(downloadId, InstallStep.Error) } tempFile.delete() diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index f3840acc3..1027bdabe 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -347,8 +347,6 @@ Filtered Not loaded Couldn\'t load - Couldn\'t install - Why it failed Why it isn\'t loaded Copy stack trace This extension\'s content warning isn\'t one you chose to load, so its sources aren\'t available.