Use Compose for Library list and grid (#7520)

This commit is contained in:
Andreas
2022-07-16 21:06:24 +02:00
committed by GitHub
parent 018ca71336
commit 905c96922b
23 changed files with 855 additions and 896 deletions

View File

@@ -0,0 +1,48 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
@Composable
fun BadgeGroup(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(4.dp),
content: @Composable RowScope.() -> Unit,
) {
Row(modifier = modifier.clip(shape)) {
content()
}
}
@Composable
fun Badge(
text: String,
color: Color = MaterialTheme.colorScheme.secondary,
textColor: Color = MaterialTheme.colorScheme.onSecondary,
shape: Shape = RectangleShape,
) {
Box(
modifier = Modifier
.background(color)
.clip(shape),
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
color = textColor,
)
}
}

View File

@@ -0,0 +1,30 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.plus
@Composable
fun LazyLibraryGrid(
modifier: Modifier = Modifier,
columns: Int,
content: LazyGridScope.() -> Unit,
) {
LazyVerticalGrid(
modifier = modifier,
columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns),
contentPadding = PaddingValues(8.dp) + WindowInsets.navigationBars.asPaddingValues(),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = content,
)
}

View File

@@ -0,0 +1,82 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
fun LibraryComfortableGrid(
items: List<LibraryItem>,
columns: Int,
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
LazyLibraryGrid(
columns = columns,
) {
items(
items = items,
key = {
it.manga.id!!
},
) { libraryItem ->
LibraryComfortableGridItem(
libraryItem,
libraryItem.manga in selection,
onClick,
onLongClick,
)
}
}
}
@Composable
fun LibraryComfortableGridItem(
item: LibraryItem,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val manga = item.manga
LibraryGridItemSelectable(isSelected = isSelected) {
Column(
modifier = Modifier
.combinedClickable(
onClick = {
onClick(manga)
},
onLongClick = {
onLongClick(manga)
},
),
) {
LibraryGridCover(
mangaCover = MangaCover(
manga.id!!,
manga.source,
manga.favorite,
manga.thumbnail_url,
manga.cover_last_modified,
),
downloadCount = item.downloadCount,
unreadCount = item.unreadCount,
isLocal = item.isLocal,
language = item.sourceLanguage,
)
Text(
text = manga.title,
maxLines = 2,
style = LocalTextStyle.current.copy(fontWeight = FontWeight.SemiBold),
)
}
}
}

View File

@@ -0,0 +1,104 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
fun LibraryCompactGrid(
items: List<LibraryItem>,
columns: Int,
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
LazyLibraryGrid(
columns = columns,
) {
items(
items = items,
key = {
it.manga.id!!
},
) { libraryItem ->
LibraryCompactGridItem(
item = libraryItem,
isSelected = libraryItem.manga in selection,
onClick = onClick,
onLongClick = onLongClick,
)
}
}
}
@Composable
fun LibraryCompactGridItem(
item: LibraryItem,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val manga = item.manga
LibraryGridCover(
modifier = Modifier
.selectedOutline(isSelected)
.combinedClickable(
onClick = {
onClick(manga)
},
onLongClick = {
onLongClick(manga)
},
),
mangaCover = eu.kanade.domain.manga.model.MangaCover(
manga.id!!,
manga.source,
manga.favorite,
manga.thumbnail_url,
manga.cover_last_modified,
),
downloadCount = item.downloadCount,
unreadCount = item.unreadCount,
isLocal = item.isLocal,
language = item.sourceLanguage,
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xAA000000),
),
)
.fillMaxHeight(0.33f)
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
Text(
text = manga.title,
modifier = Modifier
.padding(8.dp)
.align(Alignment.BottomStart),
maxLines = 2,
style = LocalTextStyle.current.copy(color = Color.White, fontWeight = FontWeight.SemiBold),
)
}
}

View File

@@ -0,0 +1,68 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
fun LibraryCoverOnlyGrid(
items: List<LibraryItem>,
columns: Int,
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
LazyLibraryGrid(
columns = columns,
) {
items(
items = items,
key = {
it.manga.id!!
},
) { libraryItem ->
LibraryCoverOnlyGridItem(
item = libraryItem,
isSelected = libraryItem.manga in selection,
onClick = onClick,
onLongClick = onLongClick,
)
}
}
}
@Composable
fun LibraryCoverOnlyGridItem(
item: LibraryItem,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val manga = item.manga
LibraryGridCover(
modifier = Modifier
.selectedOutline(isSelected)
.combinedClickable(
onClick = {
onClick(manga)
},
onLongClick = {
onLongClick(manga)
},
),
mangaCover = eu.kanade.domain.manga.model.MangaCover(
manga.id!!,
manga.source,
manga.favorite,
manga.thumbnail_url,
manga.cover_last_modified,
),
downloadCount = item.downloadCount,
unreadCount = item.unreadCount,
isLocal = item.isLocal,
language = item.sourceLanguage,
)
}

View File

@@ -0,0 +1,74 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.MangaCover
import eu.kanade.tachiyomi.R
@Composable
fun LibraryGridCover(
modifier: Modifier = Modifier,
mangaCover: eu.kanade.domain.manga.model.MangaCover,
downloadCount: Int,
unreadCount: Int,
isLocal: Boolean,
language: String,
content: @Composable BoxScope.() -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
) {
MangaCover.Book(
modifier = Modifier.fillMaxWidth(),
data = mangaCover,
)
content()
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
if (downloadCount > 0) {
Badge(
text = "$downloadCount",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
if (unreadCount > 0) {
Badge(text = "$unreadCount")
}
}
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopEnd),
) {
if (isLocal) {
Badge(
text = stringResource(id = R.string.local_source_badge),
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
if (isLocal.not() && language.isNotEmpty()) {
Badge(
text = language,
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
}
}

View File

@@ -0,0 +1,46 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.dp
fun Modifier.selectedOutline(isSelected: Boolean) = composed {
val secondary = MaterialTheme.colorScheme.secondary
if (isSelected) {
drawBehind {
val additional = 24.dp.value
val offset = additional / 2
val height = size.height + additional
val width = size.width + additional
drawRoundRect(
color = secondary,
topLeft = Offset(-offset, -offset),
size = Size(width, height),
cornerRadius = CornerRadius(offset),
)
}
} else {
this
}
}
@Composable
fun LibraryGridItemSelectable(
isSelected: Boolean,
content: @Composable () -> Unit,
) {
Box(Modifier.selectedOutline(isSelected)) {
CompositionLocalProvider(LocalContentColor provides if (isSelected) MaterialTheme.colorScheme.onSecondary else MaterialTheme.colorScheme.onBackground) {
content()
}
}
}

View File

@@ -0,0 +1,121 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
fun LibraryList(
items: List<LibraryItem>,
columns: Int,
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
LazyColumn(
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
items(
items = items,
key = {
it.manga.id!!
},
) { libraryItem ->
LibraryListItem(
item = libraryItem,
isSelected = libraryItem.manga in selection,
onClick = onClick,
onLongClick = onLongClick,
)
}
}
}
@Composable
fun LibraryListItem(
item: LibraryItem,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val manga = item.manga
Row(
modifier = Modifier
.selectedBackground(isSelected)
.height(56.dp)
.combinedClickable(
onClick = { onClick(manga) },
onLongClick = { onLongClick(manga) },
)
.padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
eu.kanade.presentation.components.MangaCover.Square(
modifier = Modifier
.padding(vertical = verticalPadding)
.fillMaxHeight(),
data = MangaCover(
manga.id!!,
manga.source,
manga.favorite,
manga.thumbnail_url,
manga.cover_last_modified,
),
)
Text(
text = manga.title,
modifier = Modifier
.padding(horizontal = horizontalPadding)
.weight(1f),
maxLines = 2,
style = MaterialTheme.typography.bodyMedium,
)
BadgeGroup {
if (item.downloadCount > 0) {
Badge(
text = "${item.downloadCount}",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
if (item.unreadCount > 0) {
Badge(text = "${item.unreadCount}")
}
if (item.isLocal) {
Badge(
text = stringResource(id = R.string.local_source_badge),
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
if (item.isLocal.not() && item.sourceLanguage.isNotEmpty()) {
Badge(
text = item.sourceLanguage,
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
}
}

View File

@@ -1,8 +1,11 @@
package eu.kanade.presentation.util
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@@ -17,6 +20,15 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.DpSize
import kotlin.math.roundToInt
fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed {
if (isSelected) {
val alpha = if (isSystemInDarkTheme()) 0.08f else 0.22f
background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
} else {
this
}
}
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(.78f)
fun Modifier.clickableNoIndication(