2
0
mirror of https://github.com/fork-maintainers/iceraven-browser synced 2024-11-17 15:26:23 +00:00

[fenix] closes https://github.com/mozilla-mobile/fenix/issues/24442: refactor recent tabs to use common composables

This commit is contained in:
MatthewTighe 2022-03-24 15:05:42 -07:00 committed by mergify[bot]
parent ad7e540613
commit c77cf53ae5
2 changed files with 181 additions and 130 deletions

View File

@ -0,0 +1,130 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Card which will display a thumbnail. If a thumbnail is not available for [url], the favicon
* will be displayed until the thumbnail is loaded.
*
* @param url Url to display thumbnail for.
* @param key Key used to remember the thumbnail for future compositions.
* @param modifier [Modifier] used to draw the image content.
* @param contentScale [ContentScale] used to draw image content.
* @param alignment [Alignment] used to draw the image content.
*/
@Composable
fun ThumbnailCard(
url: String,
key: String,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.FillWidth,
alignment: Alignment = Alignment.TopCenter
) {
Card(
modifier = modifier,
backgroundColor = colorResource(id = R.color.photonGrey20)
) {
components.core.icons.Loader(url) {
Placeholder {
Box(
modifier = Modifier.background(color = FirefoxTheme.colors.layer3)
)
}
WithIcon { icon ->
Box(
modifier = Modifier.size(36.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = icon.painter,
contentDescription = null,
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Fit
)
}
}
}
ThumbnailImage(
key = key,
modifier = modifier,
contentScale = contentScale,
alignment = alignment
)
}
}
@Composable
private fun ThumbnailImage(
key: String,
modifier: Modifier,
contentScale: ContentScale,
alignment: Alignment
) {
val rememberBitmap = remember(key) { mutableStateOf<ImageBitmap?>(null) }
val size = LocalDensity.current.run { 108.dp.toPx().toInt() }
val request = ImageLoadRequest(key, size)
val storage = components.core.thumbnailStorage
val bitmap = rememberBitmap.value
LaunchedEffect(key) {
rememberBitmap.value = storage.loadThumbnail(request).await()?.asImageBitmap()
}
if (bitmap != null) {
val painter = BitmapPainter(bitmap)
Image(
painter = painter,
contentDescription = null,
modifier = modifier,
contentScale = contentScale,
alignment = alignment
)
}
}
@Preview
@Composable
fun ThumbnailCardPreview() {
ThumbnailCard(
url = "https://mozilla.com",
key = "123",
modifier = Modifier
.size(108.dp, 80.dp)
.clip(RoundedCornerShape(8.dp))
)
}

View File

@ -36,7 +36,6 @@ import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -44,13 +43,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -60,13 +56,14 @@ import androidx.compose.ui.unit.sp
import mozilla.components.browser.icons.compose.Loader import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.concept.base.images.ImageLoadRequest
import mozilla.components.support.ktx.kotlin.getRepresentativeSnippet
import mozilla.components.ui.colors.PhotonColors import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.Image import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.PrimaryText
import org.mozilla.fenix.compose.SecondaryText
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
@ -134,7 +131,6 @@ fun RecentTabs(
* @param onRecentTabClick Invoked when the user clicks on a recent tab. * @param onRecentTabClick Invoked when the user clicks on a recent tab.
*/ */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Suppress("LongParameterList")
@Composable @Composable
private fun RecentTabItem( private fun RecentTabItem(
tab: RecentTab.Tab, tab: RecentTab.Tab,
@ -172,7 +168,12 @@ private fun RecentTabItem(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
RecentTabTitle(title = tab.state.content.title.ifEmpty { tab.state.content.url }) PrimaryText(
text = tab.state.content.title.ifEmpty { tab.state.content.url },
fontSize = 14.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Row { Row {
RecentTabIcon( RecentTabIcon(
@ -183,7 +184,12 @@ private fun RecentTabItem(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
RecentTabSubtitle(subtitle = tab.state.content.url) SecondaryText(
text = tab.state.content.url,
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
} }
} }
@ -205,7 +211,6 @@ private fun RecentTabItem(
* @param count Count of how many tabs belongs to the group. * @param count Count of how many tabs belongs to the group.
* @param onSearchGroupClick Invoked when the user clicks on a group. * @param onSearchGroupClick Invoked when the user clicks on a group.
*/ */
@Suppress("LongParameterList")
@Composable @Composable
private fun RecentSearchGroupItem( private fun RecentSearchGroupItem(
searchTerm: String, searchTerm: String,
@ -239,7 +244,12 @@ private fun RecentSearchGroupItem(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
RecentTabTitle(title = stringResource(R.string.recent_tabs_search_term, searchTerm)) PrimaryText(
text = stringResource(R.string.recent_tabs_search_term, searchTerm),
fontSize = 14.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
Row { Row {
Icon( Icon(
@ -254,7 +264,12 @@ private fun RecentSearchGroupItem(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
RecentTabSubtitle(subtitle = stringResource(R.string.recent_tabs_search_term_count_2, count)) SecondaryText(
text = stringResource(R.string.recent_tabs_search_term_count_2, count),
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
} }
} }
} }
@ -289,8 +304,9 @@ private fun RecentSyncedTabItem(
if (tab == null) { if (tab == null) {
RecentTabImagePlaceholder() RecentTabImagePlaceholder()
} else { } else {
RecentTabImage( ThumbnailCard(
tab = tab, url = tab.url,
key = tab.url.hashCode().toString(),
modifier = Modifier modifier = Modifier
.size(108.dp, 80.dp) .size(108.dp, 80.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
@ -306,7 +322,12 @@ private fun RecentSyncedTabItem(
if (tab == null) { if (tab == null) {
RecentTabTitlePlaceholder() RecentTabTitlePlaceholder()
} else { } else {
RecentTabTitle(title = tab.title) PrimaryText(
text = tab.title,
fontSize = 14.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@ -331,7 +352,12 @@ private fun RecentSyncedTabItem(
if (tab == null) { if (tab == null) {
TextLinePlaceHolder() TextLinePlaceHolder()
} else { } else {
RecentTabSubtitle(subtitle = tab.deviceDisplayName) SecondaryText(
text = tab.deviceDisplayName,
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
} }
} }
} }
@ -377,23 +403,14 @@ private fun RecentSyncedTabItem(
* @param alignment [Alignment] used to draw the image content. * @param alignment [Alignment] used to draw the image content.
*/ */
@Composable @Composable
@Suppress("LongParameterList") fun RecentTabImage(
private fun RecentTabImage( tab: RecentTab.Tab,
tab: RecentTab,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.FillWidth, contentScale: ContentScale = ContentScale.FillWidth,
alignment: Alignment = Alignment.TopCenter alignment: Alignment = Alignment.TopCenter
) { ) {
val (previewImageUrl, loaderUrl, key) = when (tab) { val previewImageUrl = tab.state.content.previewImageUrl
is RecentTab.Tab -> Triple( val thumbnail = tab.state.content.thumbnail
tab.state.content.previewImageUrl,
tab.state.content.url,
tab.state.id
)
is RecentTab.SyncedTab -> Triple(tab.previewImageUrl, tab.url, tab.url)
else -> return
}
val thumbnail = (tab as? RecentTab.Tab)?.state?.content?.thumbnail
when { when {
!previewImageUrl.isNullOrEmpty() -> { !previewImageUrl.isNullOrEmpty() -> {
@ -413,43 +430,11 @@ private fun RecentTabImage(
alignment = alignment alignment = alignment
) )
} }
else -> { else -> ThumbnailCard(
Card( url = tab.state.content.url,
modifier = modifier, key = tab.state.id,
backgroundColor = colorResource(id = R.color.photonGrey20) modifier = modifier
) { )
components.core.icons.Loader(loaderUrl) {
Placeholder {
Box(
modifier = Modifier.background(color = FirefoxTheme.colors.layer3)
)
}
WithIcon { icon ->
Box(
modifier = Modifier.size(36.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = icon.painter,
contentDescription = null,
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Fit
)
}
}
}
ThumbnailImage(
key = key,
modifier = modifier,
contentScale = contentScale,
alignment = alignment
)
}
}
} }
} }
@ -567,22 +552,6 @@ private fun RecentTabIcon(
} }
} }
/**
* A recent tab title.
*
* @param title The title of the tab.
*/
@Composable
private fun RecentTabTitle(title: String) {
Text(
text = title,
color = FirefoxTheme.colors.textPrimary,
fontSize = 14.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 2
)
}
/** /**
* A placeholder for a tab title. * A placeholder for a tab title.
*/ */
@ -597,54 +566,6 @@ private fun RecentTabTitlePlaceholder() {
} }
} }
/**
* A recent tab subtitle.
*
* @param subtitle The loaded URL of the tab.
*/
@Composable
private fun RecentTabSubtitle(subtitle: String) {
Text(
text = subtitle.getRepresentativeSnippet(),
color = when (isSystemInDarkTheme()) {
true -> FirefoxTheme.colors.textPrimary
false -> FirefoxTheme.colors.textSecondary
},
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
@Composable
private fun ThumbnailImage(
key: String,
modifier: Modifier,
contentScale: ContentScale,
alignment: Alignment
) {
val rememberBitmap = remember(key) { mutableStateOf<ImageBitmap?>(null) }
val size = LocalDensity.current.run { 108.dp.toPx().toInt() }
val request = ImageLoadRequest(key, size)
val storage = components.core.thumbnailStorage
val bitmap = rememberBitmap.value
LaunchedEffect(key) {
rememberBitmap.value = storage.loadThumbnail(request).await()?.asImageBitmap()
}
if (bitmap != null) {
val painter = BitmapPainter(bitmap)
Image(
painter = painter,
contentDescription = null,
modifier = modifier,
contentScale = contentScale,
alignment = alignment
)
}
}
@Composable @Composable
private fun TextLinePlaceHolder() { private fun TextLinePlaceHolder() {
Box( Box(