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 package eu.kanade.presentation.browse
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.combinedClickable 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope 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.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
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.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
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.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.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
@ -32,6 +36,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.browse.components.BaseBrowseItem 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.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.SwipeRefreshIndicator import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.theme.header import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.bottomNavPaddingValues import eu.kanade.presentation.util.bottomNavPaddingValues
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@ -117,9 +124,8 @@ private fun ExtensionContent(
}, },
key = { key = {
when (it) { when (it) {
is ExtensionUiModel.Header.Resource -> it.textRes is ExtensionUiModel.Header -> "extensionHeader-${it.hashCode()}"
is ExtensionUiModel.Header.Text -> it.text is ExtensionUiModel.Item -> "extension-${it.extension.hashCode()}"
is ExtensionUiModel.Item -> "extension-${it.key()}"
} }
}, },
) { item -> ) { item ->
@ -219,7 +225,27 @@ private fun ExtensionItem(
onClickItem = { onClickItem(extension) }, onClickItem = { onClickItem(extension) },
onLongClickItem = { onLongClickItem(extension) }, onLongClickItem = { onLongClickItem(extension) },
icon = { 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 = { action = {
ExtensionItemActions( ExtensionItemActions(
@ -232,6 +258,7 @@ private fun ExtensionItem(
) { ) {
ExtensionItemContent( ExtensionItemContent(
extension = extension, extension = extension,
installStep = installStep,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
@ -240,19 +267,9 @@ private fun ExtensionItem(
@Composable @Composable
private fun ExtensionItemContent( private fun ExtensionItemContent(
extension: Extension, extension: Extension,
installStep: InstallStep,
modifier: Modifier = Modifier, 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( Column(
modifier = modifier.padding(start = horizontalPadding), modifier = modifier.padding(start = horizontalPadding),
) { ) {
@ -262,32 +279,51 @@ private fun ExtensionItemContent(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
Row( // Won't look good but it's not like we can ellipsize overflowing content
horizontalArrangement = Arrangement.spacedBy(4.dp), FlowRow(
modifier = Modifier.secondaryItemAlpha(),
mainAxisSpacing = 4.dp,
) { ) {
if (extension.lang.isNullOrEmpty().not()) { ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
Text( if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
text = LocaleHelper.getSourceDisplayName(extension.lang, context), Text(
style = MaterialTheme.typography.bodySmall, text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
) )
} }
if (extension.versionName.isNotEmpty()) { if (extension.versionName.isNotEmpty()) {
Text( Text(
text = extension.versionName, text = extension.versionName,
style = MaterialTheme.typography.bodySmall, )
) }
}
if (warning != null) { val warning = when {
Text( extension is Extension.Untrusted -> R.string.ext_untrusted
text = stringResource(warning).uppercase(), extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial
maxLines = 1, extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete
overflow = TextOverflow.Ellipsis, extension.isNsfw -> R.string.ext_nsfw_short
style = MaterialTheme.typography.bodySmall.copy( else -> null
}
if (warning != null) {
Text(
text = stringResource(warning).uppercase(),
color = MaterialTheme.colorScheme.error, 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 = {}, onClickItemCancel: (Extension) -> Unit = {},
onClickItemAction: (Extension) -> Unit = {}, onClickItemAction: (Extension) -> Unit = {},
) { ) {
val isIdle = remember(installStep) { val isIdle = installStep.isCompleted()
installStep == InstallStep.Idle || installStep == InstallStep.Error
}
Row(modifier = modifier) { Row(modifier = modifier) {
TextButton( if (isIdle) {
onClick = { onClickItemAction(extension) }, TextButton(
enabled = isIdle, onClick = { onClickItemAction(extension) },
) { ) {
Text( Text(
text = when (installStep) { text = when (installStep) {
InstallStep.Pending -> stringResource(R.string.ext_pending) InstallStep.Installed -> stringResource(R.string.ext_installed)
InstallStep.Downloading -> stringResource(R.string.ext_downloading) InstallStep.Error -> stringResource(R.string.action_retry)
InstallStep.Installing -> stringResource(R.string.ext_installing) InstallStep.Idle -> {
InstallStep.Installed -> stringResource(R.string.ext_installed) when (extension) {
InstallStep.Error -> stringResource(R.string.action_retry) is Extension.Installed -> {
InstallStep.Idle -> { if (extension.hasUpdate) {
when (extension) { stringResource(R.string.ext_update)
is Extension.Installed -> { } else {
if (extension.hasUpdate) { stringResource(R.string.action_settings)
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)
} }
} else -> error("Must not show install process text")
}, },
style = LocalTextStyle.current.copy( )
color = if (isIdle) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceTint, }
), } else {
)
}
if (isIdle.not()) {
IconButton(onClick = { onClickItemCancel(extension) }) { IconButton(onClick = { onClickItemCancel(extension) }) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "", contentDescription = stringResource(id = R.string.action_cancel),
tint = MaterialTheme.colorScheme.onBackground,
) )
} }
} }

View File

@ -81,12 +81,11 @@ fun ExtensionIcon(
is Extension.Available -> { is Extension.Available -> {
AsyncImage( AsyncImage(
model = extension.iconUrl, model = extension.iconUrl,
contentDescription = "", contentDescription = null,
placeholder = ColorPainter(Color(0x1F888888)), placeholder = ColorPainter(Color(0x1F888888)),
error = rememberResourceBitmapPainter(id = R.drawable.cover_error), error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp)),
.then(defaultModifier),
) )
} }
is Extension.Installed -> { is Extension.Installed -> {
@ -94,20 +93,20 @@ fun ExtensionIcon(
when (icon) { when (icon) {
Result.Error -> Image( Result.Error -> Image(
bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_local_source), bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_local_source),
contentDescription = "", contentDescription = null,
modifier = modifier.then(defaultModifier), modifier = modifier,
) )
Result.Loading -> Box(modifier = modifier.then(defaultModifier)) Result.Loading -> Box(modifier = modifier)
is Result.Success -> Image( is Result.Success -> Image(
bitmap = (icon as Result.Success<ImageBitmap>).value, bitmap = (icon as Result.Success<ImageBitmap>).value,
contentDescription = "", contentDescription = null,
modifier = modifier.then(defaultModifier), modifier = modifier,
) )
} }
} }
is Extension.Untrusted -> Image( is Extension.Untrusted -> Image(
imageVector = Icons.Default.Dangerous, imageVector = Icons.Default.Dangerous,
contentDescription = "", contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = modifier.then(defaultModifier), modifier = modifier.then(defaultModifier),
) )

View File

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

View File

@ -212,13 +212,5 @@ sealed interface ExtensionUiModel {
data class Item( data class Item(
val extension: Extension, val extension: Extension,
val installStep: InstallStep, val installStep: InstallStep,
) : ExtensionUiModel { ) : ExtensionUiModel
fun key(): String {
return when {
extension is Extension.Installed && extension.hasUpdate -> "${extension.pkgName}_update"
else -> "${extension.pkgName}_${installStep.name}"
}
}
}
} }