Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal - compact card layout #243

Merged
merged 3 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ enum class FeedItemStyle(
@StringRes val stringId: Int,
) {
CARD(R.string.feed_item_style_card),
COMPACT_CARD(R.string.feed_item_style_compact_card),
COMPACT(R.string.feed_item_style_compact),
SUPER_COMPACT(R.string.feed_item_style_super_compact),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
package com.nononsenseapps.feeder.ui.compose.feed

import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Terrain
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.size.Size
import coil.size.pxOrElse
import com.nononsenseapps.feeder.R
import com.nononsenseapps.feeder.db.room.ID_UNSET
import com.nononsenseapps.feeder.model.EnclosureImage
import com.nononsenseapps.feeder.ui.compose.coil.rememberTintedVectorPainter
import com.nononsenseapps.feeder.ui.compose.minimumTouchSize
import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme
import com.nononsenseapps.feeder.ui.compose.utils.ThemePreviews
import java.net.URL
import java.time.Instant
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull

sealed interface FeedItemEvent {
data object MarkAboveAsRead: FeedItemEvent
data object MarkBelowAsRead: FeedItemEvent
data object ShareItem: FeedItemEvent
data object ToggleBookmarked: FeedItemEvent
data object DismissDropdown: FeedItemEvent
}

@Immutable
data class FeedItemState(
val item: FeedListItem,
val showThumbnail: Boolean = true,
val bookmarkIndicator: Boolean = true,
val dropDownMenuExpanded: Boolean = false,
val showReadingTime: Boolean = false,
val maxLines: Int = 2
)

private val iconSize = 24.dp
private val gradientColors = listOf(Color.Black.copy(alpha = 0.8f), Color.Black.copy(alpha = 0.38f), Color.Black.copy(alpha = 0.8f))

@Composable
fun FeedItemCompactCard(
state: FeedItemState,
onEvent: (FeedItemEvent) -> Unit = { },
modifier: Modifier = Modifier,
) {
ElevatedCard(
modifier = modifier,
shape = MaterialTheme.shapes.medium
) {
BoxWithConstraints(
modifier = Modifier
.requiredHeightIn(min = minimumTouchSize)
.fillMaxWidth()
) {
if (state.showThumbnail) {
val sizePx = with(LocalDensity.current) {
val width = maxWidth.roundToPx()
Size(width, (width * 9) / 16)
}
val gradient = Brush.verticalGradient(
colors = gradientColors,
startY = 0f,
endY = sizePx.height.pxOrElse { 0 }.toFloat()
)
val imageUrl = state.item.image?.url
if (imageUrl != null) {
FeedItemThumbnail(
imageUrl = imageUrl,
sizePx = sizePx
)
}
Box(modifier = Modifier
.matchParentSize()
.background(gradient))
}

Row(modifier = Modifier
.height(iconSize)
.padding(top = 8.dp, end = 8.dp, start = 8.dp)
.align(Alignment.TopEnd)
) {
if (state.item.bookmarked && state.bookmarkIndicator) {
FeedItemSavedIndicator(size = iconSize, modifier = modifier)
}
if (state.item.unread) {
FeedItemNewIndicator(size = iconSize, modifier = modifier)
}
state.item.feedImageUrl?.toHttpUrlOrNull()?.also {
FeedItemFeedIconIndicator(
feedImageUrl = it.toString(),
size = iconSize,
modifier = modifier,
)
}
}

Box(modifier = Modifier
.width(maxWidth)
.padding(top = 48.dp)
.align(Alignment.BottomCenter)
) {
FeedItemTitle(
state = state,
onEvent = onEvent,
)
}
}
}
}

@Composable
private fun FeedItemTitle(
state: FeedItemState,
onEvent: (FeedItemEvent) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 8.dp),
) {
val textColor = when {
!state.showThumbnail -> LocalContentColor.current
state.item.unread -> Color.White
else -> Color.White.copy(alpha = 0.74f)
}
CompositionLocalProvider(LocalContentColor provides textColor) {
FeedItemText(
item = state.item,
onMarkAboveAsRead = { onEvent(FeedItemEvent.MarkAboveAsRead) },
onMarkBelowAsRead = { onEvent(FeedItemEvent.MarkBelowAsRead) },
onShareItem = { onEvent(FeedItemEvent.ShareItem) },
onToggleBookmarked = { onEvent(FeedItemEvent.ToggleBookmarked) },
dropDownMenuExpanded = state.dropDownMenuExpanded,
onDismissDropdown = { onEvent(FeedItemEvent.DismissDropdown) },
maxLines = state.maxLines,
showOnlyTitle = true,
showReadingTime = state.showReadingTime
)
}
}
}

@Composable
private fun FeedItemThumbnail(imageUrl: String?, sizePx: Size) {
if (imageUrl != null) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.listener(
onError = { a, b ->
Log.e("FEEDER_CARD", "error ${a.data}", b.throwable)
},
)
.scale(Scale.FILL)
.size(sizePx)
.precision(Precision.INEXACT)
.build(),
placeholder = rememberTintedVectorPainter(Icons.Outlined.Terrain),
error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline),
contentDescription = stringResource(id = R.string.article_image),
contentScale = ContentScale.Crop,
alignment = Alignment.Center,
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.fillMaxWidth()
.aspectRatio(16.0f / 9.0f)
.alpha(0.74f),
)
}
}

@Composable
@ThemePreviews
private fun Preview() {
FeederTheme {
FeedItemCompactCard(
state = FeedItemState(
item = FeedListItem(
title = "title",
snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing",
feedTitle = "Super Duper Feed One two three hup di too dasf dsaf asd fsa dfasdf",
pubDate = "Jun 9, 2021",
unread = true,
image = null,
link = null,
id = ID_UNSET,
bookmarked = true,
feedImageUrl = null,
primarySortTime = Instant.EPOCH,
rawPubDate = null,
wordCount = 0
)
)
)
}
}

@Composable
@ThemePreviews
private fun PreviewWithImageUnread() {
FeederTheme {
Box(
modifier = Modifier.width((300 - 2 * 16).dp),
) {
FeedItemCompactCard(
state = FeedItemState(
item = FeedListItem(
title = "title can be one line",
snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing",
feedTitle = "Super Feed",
pubDate = "Jun 9, 2021",
unread = true,
image = EnclosureImage(url = "blabal", length = 0),
link = null,
id = ID_UNSET,
bookmarked = false,
feedImageUrl = URL("https://foo/bar.png"),
primarySortTime = Instant.EPOCH,
rawPubDate = null,
wordCount = 0
)
)
)
}
}
}

@Composable
@ThemePreviews
private fun PreviewWithImageRead() {
FeederTheme {
Box(
modifier = Modifier.width((300 - 2 * 16).dp),
) {
FeedItemCompactCard(
state = FeedItemState(
item = FeedListItem(
title = "title can be one line",
snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing",
feedTitle = "Super Duper Feed",
pubDate = "Jun 9, 2021",
unread = false,
image = EnclosureImage(url = "blabal", length = 0),
link = null,
id = ID_UNSET,
bookmarked = true,
feedImageUrl = null,
primarySortTime = Instant.EPOCH,
rawPubDate = null,
wordCount = 0
)
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ fun FeedListContent(
val arrangement =
when (viewState.feedItemStyle) {
FeedItemStyle.CARD -> Arrangement.spacedBy(LocalDimens.current.margin)
FeedItemStyle.COMPACT_CARD -> Arrangement.spacedBy(LocalDimens.current.margin)
FeedItemStyle.COMPACT -> Arrangement.spacedBy(0.dp)
FeedItemStyle.SUPER_COMPACT -> Arrangement.spacedBy(0.dp)
}
Expand All @@ -1177,6 +1178,7 @@ fun FeedListContent(
).run {
when (viewState.feedItemStyle) {
FeedItemStyle.CARD -> addMargin(horizontal = LocalDimens.current.margin)
FeedItemStyle.COMPACT_CARD -> addMargin(horizontal = LocalDimens.current.margin)
// No margin since dividers
FeedItemStyle.COMPACT, FeedItemStyle.SUPER_COMPACT -> this
}
Expand Down Expand Up @@ -1285,7 +1287,8 @@ fun FeedListContent(
onItemClick(previewItem.id)
}

if (viewState.feedItemStyle != FeedItemStyle.CARD) {
if (viewState.feedItemStyle != FeedItemStyle.CARD
&& viewState.feedItemStyle != FeedItemStyle.COMPACT_CARD) {
if (itemIndex < pagedFeedItems.itemCount - 1) {
Divider(
modifier =
Expand Down Expand Up @@ -1371,6 +1374,7 @@ fun FeedGridContent(
val arrangement =
when (feedItemStyle) {
FeedItemStyle.CARD -> Arrangement.spacedBy(LocalDimens.current.gutter)
FeedItemStyle.COMPACT_CARD -> Arrangement.spacedBy(LocalDimens.current.gutter)
FeedItemStyle.COMPACT -> Arrangement.spacedBy(LocalDimens.current.gutter)
FeedItemStyle.SUPER_COMPACT -> Arrangement.spacedBy(LocalDimens.current.gutter)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,31 @@ fun SwipeableFeedItemPreview(
)
}

FeedItemStyle.COMPACT_CARD -> {
FeedItemCompactCard(
state = FeedItemState(
item = item,
showThumbnail = showThumbnail && !compactLandscape,
dropDownMenuExpanded = dropDownMenuExpanded,
bookmarkIndicator = bookmarkIndicator,
maxLines = maxLines,
showReadingTime = showReadingTime
),
onEvent = { event ->
when (event) {
FeedItemEvent.DismissDropdown -> { dropDownMenuExpanded = false }
FeedItemEvent.MarkAboveAsRead -> onMarkAboveAsRead()
FeedItemEvent.MarkBelowAsRead -> onMarkBelowAsRead()
FeedItemEvent.ShareItem -> onShareItem()
FeedItemEvent.ToggleBookmarked -> onToggleBookmarked()
}
},
modifier = Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.graphicsLayer(alpha = itemAlpha),
)
}

FeedItemStyle.COMPACT -> {
FeedItemCompact(
item = item,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@

<string name="feed_item_style">Article style</string>
<string name="feed_item_style_card">Card</string>
<string name="feed_item_style_compact_card">Compact Card</string>
<string name="feed_item_style_compact">Compact</string>
<string name="feed_item_style_super_compact">Super compact</string>
<string name="generate_extra_unique_ids">Generate extra unique IDs</string>
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,6 @@ dependencyResolutionManagement {
}
}

rootProject.name = "feeder"
rootProject.name = "Feeder"
anod marked this conversation as resolved.
Show resolved Hide resolved

include(":app")
Loading