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 25c28fad08..f17cc18e3e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -1,8 +1,9 @@ package eu.kanade.presentation.browse import androidx.annotation.StringRes +import androidx.compose.animation.core.animateDpAsState 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.Row import androidx.compose.foundation.layout.RowScope @@ -10,15 +11,18 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close 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 @@ -32,6 +36,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import eu.kanade.presentation.browse.components.BaseBrowseItem @@ -40,10 +45,12 @@ import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.FastScrollLazyColumn import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.SwipeRefreshIndicator +import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText import eu.kanade.presentation.theme.header import eu.kanade.presentation.util.bottomNavPaddingValues import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.plus +import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.topPaddingValues import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.extension.model.Extension @@ -117,9 +124,8 @@ private fun ExtensionContent( }, key = { when (it) { - is ExtensionUiModel.Header.Resource -> it.textRes - is ExtensionUiModel.Header.Text -> it.text - is ExtensionUiModel.Item -> "extension-${it.key()}" + is ExtensionUiModel.Header -> "extensionHeader-${it.hashCode()}" + is ExtensionUiModel.Item -> "extension-${it.extension.hashCode()}" } }, ) { item -> @@ -219,7 +225,27 @@ private fun ExtensionItem( onClickItem = { onClickItem(extension) }, onLongClickItem = { onLongClickItem(extension) }, icon = { - ExtensionIcon(extension = extension) + 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) + ExtensionIcon( + extension = extension, + modifier = Modifier + .matchParentSize() + .padding(padding), + ) + } }, action = { ExtensionItemActions( @@ -232,6 +258,7 @@ private fun ExtensionItem( ) { ExtensionItemContent( extension = extension, + installStep = installStep, modifier = Modifier.weight(1f), ) } @@ -240,19 +267,9 @@ private fun ExtensionItem( @Composable private fun ExtensionItemContent( extension: Extension, + installStep: InstallStep, modifier: Modifier = Modifier, ) { - val context = LocalContext.current - val warning = remember(extension) { - when { - extension is Extension.Untrusted -> R.string.ext_untrusted - extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial - extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete - extension.isNsfw -> R.string.ext_nsfw_short - else -> null - } - } - Column( modifier = modifier.padding(start = horizontalPadding), ) { @@ -262,32 +279,51 @@ private fun ExtensionItemContent( overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium, ) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), + // Won't look good but it's not like we can ellipsize overflowing content + FlowRow( + modifier = Modifier.secondaryItemAlpha(), + mainAxisSpacing = 4.dp, ) { - if (extension.lang.isNullOrEmpty().not()) { - Text( - text = LocaleHelper.getSourceDisplayName(extension.lang, context), - style = MaterialTheme.typography.bodySmall, - ) - } + ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { + if (extension is Extension.Installed && extension.lang.isNotEmpty()) { + Text( + text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current), + ) + } - if (extension.versionName.isNotEmpty()) { - Text( - text = extension.versionName, - style = MaterialTheme.typography.bodySmall, - ) - } + if (extension.versionName.isNotEmpty()) { + Text( + text = extension.versionName, + ) + } - if (warning != null) { - Text( - text = stringResource(warning).uppercase(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodySmall.copy( + val warning = when { + extension is Extension.Untrusted -> R.string.ext_untrusted + extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial + extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete + extension.isNsfw -> R.string.ext_nsfw_short + else -> null + } + if (warning != null) { + Text( + text = stringResource(warning).uppercase(), color = MaterialTheme.colorScheme.error, - ), - ) + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + if (!installStep.isCompleted()) { + DotSeparatorNoSpaceText() + Text( + text = when (installStep) { + InstallStep.Pending -> stringResource(R.string.ext_pending) + InstallStep.Downloading -> stringResource(R.string.ext_downloading) + InstallStep.Installing -> stringResource(R.string.ext_installing) + else -> error("Must not show non-install process text") + }, + ) + } } } } @@ -301,46 +337,38 @@ private fun ExtensionItemActions( onClickItemCancel: (Extension) -> Unit = {}, onClickItemAction: (Extension) -> Unit = {}, ) { - val isIdle = remember(installStep) { - installStep == InstallStep.Idle || installStep == InstallStep.Error - } + val isIdle = installStep.isCompleted() Row(modifier = modifier) { - TextButton( - onClick = { onClickItemAction(extension) }, - enabled = isIdle, - ) { - Text( - text = when (installStep) { - InstallStep.Pending -> stringResource(R.string.ext_pending) - InstallStep.Downloading -> stringResource(R.string.ext_downloading) - InstallStep.Installing -> stringResource(R.string.ext_installing) - InstallStep.Installed -> stringResource(R.string.ext_installed) - InstallStep.Error -> stringResource(R.string.action_retry) - InstallStep.Idle -> { - when (extension) { - is Extension.Installed -> { - if (extension.hasUpdate) { - stringResource(R.string.ext_update) - } else { - stringResource(R.string.action_settings) + if (isIdle) { + TextButton( + onClick = { onClickItemAction(extension) }, + ) { + Text( + text = when (installStep) { + InstallStep.Installed -> stringResource(R.string.ext_installed) + InstallStep.Error -> stringResource(R.string.action_retry) + InstallStep.Idle -> { + when (extension) { + is Extension.Installed -> { + if (extension.hasUpdate) { + stringResource(R.string.ext_update) + } else { + stringResource(R.string.action_settings) + } } + is Extension.Untrusted -> stringResource(R.string.ext_trust) + is Extension.Available -> stringResource(R.string.ext_install) } - is Extension.Untrusted -> stringResource(R.string.ext_trust) - is Extension.Available -> stringResource(R.string.ext_install) } - } - }, - style = LocalTextStyle.current.copy( - color = if (isIdle) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceTint, - ), - ) - } - if (isIdle.not()) { + else -> error("Must not show install process text") + }, + ) + } + } else { IconButton(onClick = { onClickItemCancel(extension) }) { Icon( imageVector = Icons.Default.Close, - contentDescription = "", - tint = MaterialTheme.colorScheme.onBackground, + contentDescription = stringResource(id = R.string.action_cancel), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt index a7adb14e72..ccc2884fce 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt @@ -81,12 +81,11 @@ fun ExtensionIcon( is Extension.Available -> { AsyncImage( model = extension.iconUrl, - contentDescription = "", + contentDescription = null, placeholder = ColorPainter(Color(0x1F888888)), error = rememberResourceBitmapPainter(id = R.drawable.cover_error), modifier = modifier - .clip(RoundedCornerShape(4.dp)) - .then(defaultModifier), + .clip(RoundedCornerShape(4.dp)), ) } is Extension.Installed -> { @@ -94,20 +93,20 @@ fun ExtensionIcon( when (icon) { Result.Error -> Image( bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_local_source), - contentDescription = "", - modifier = modifier.then(defaultModifier), + contentDescription = null, + modifier = modifier, ) - Result.Loading -> Box(modifier = modifier.then(defaultModifier)) + Result.Loading -> Box(modifier = modifier) is Result.Success -> Image( bitmap = (icon as Result.Success).value, - contentDescription = "", - modifier = modifier.then(defaultModifier), + contentDescription = null, + modifier = modifier, ) } } is Extension.Untrusted -> Image( imageVector = Icons.Default.Dangerous, - contentDescription = "", + contentDescription = null, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), modifier = modifier.then(defaultModifier), ) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt b/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt index f4ae8ef720..fd9fd09f7f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt @@ -7,3 +7,8 @@ import androidx.compose.runtime.Composable fun DotSeparatorText() { Text(text = " • ") } + +@Composable +fun DotSeparatorNoSpaceText() { + Text(text = "•") +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt index dfeee1bd31..46b334e72c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt @@ -212,13 +212,5 @@ sealed interface ExtensionUiModel { data class Item( val extension: Extension, val installStep: InstallStep, - ) : ExtensionUiModel { - - fun key(): String { - return when { - extension is Extension.Installed && extension.hasUpdate -> "${extension.pkgName}_update" - else -> "${extension.pkgName}_${installStep.name}" - } - } - } + ) : ExtensionUiModel }