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:
parent
ad7e540613
commit
c77cf53ae5
130
app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt
Normal file
130
app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt
Normal 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))
|
||||||
|
)
|
||||||
|
}
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user