ExtensionScreen: Adjust item visual (#8120)

* ExtensionScreen: Adjust item visual

* Move install status view and add progress indicator
* Add secondary item modifier to info texts
* Wrap info texts with FlowRow in case of unavailable space
* Remove language text in non-installed items

Extra content:
* Change the list key to be more consistent
* General cleanups

* typo
This commit is contained in:
Ivan Iskandar 2022-10-01 21:32:08 +07:00 committed by GitHub
parent 80b2ebc45b
commit 58c47c4c50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 113 additions and 89 deletions

View file

@ -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,33 +279,52 @@ 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()) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
Text(
text = LocaleHelper.getSourceDisplayName(extension.lang, context),
style = MaterialTheme.typography.bodySmall,
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
)
}
if (extension.versionName.isNotEmpty()) {
Text(
text = extension.versionName,
style = MaterialTheme.typography.bodySmall,
)
}
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,
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.error,
),
)
}
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,19 +337,14 @@ 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) {
if (isIdle) {
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 -> {
@ -329,18 +360,15 @@ private fun ExtensionItemActions(
is Extension.Available -> stringResource(R.string.ext_install)
}
}
else -> error("Must not show install process text")
},
style = LocalTextStyle.current.copy(
color = if (isIdle) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceTint,
),
)
}
if (isIdle.not()) {
} else {
IconButton(onClick = { onClickItemCancel(extension) }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "",
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.action_cancel),
)
}
}

View file

@ -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<ImageBitmap>).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),
)

View file

@ -7,3 +7,8 @@ import androidx.compose.runtime.Composable
fun DotSeparatorText() {
Text(text = "")
}
@Composable
fun DotSeparatorNoSpaceText() {
Text(text = "")
}

View file

@ -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
}