|
|
|
@ -2,7 +2,7 @@
|
|
|
|
|
* 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/. */
|
|
|
|
|
|
|
|
|
|
@file:Suppress("MagicNumber")
|
|
|
|
|
@file:Suppress("MagicNumber", "TooManyFunctions")
|
|
|
|
|
|
|
|
|
|
package org.mozilla.fenix.home.recenttabs.view
|
|
|
|
|
|
|
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
|
|
import androidx.compose.foundation.layout.IntrinsicSize
|
|
|
|
|
import androidx.compose.foundation.layout.Row
|
|
|
|
|
import androidx.compose.foundation.layout.Spacer
|
|
|
|
|
import androidx.compose.foundation.layout.fillMaxHeight
|
|
|
|
@ -26,6 +27,8 @@ import androidx.compose.foundation.layout.padding
|
|
|
|
|
import androidx.compose.foundation.layout.size
|
|
|
|
|
import androidx.compose.foundation.layout.width
|
|
|
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
|
|
import androidx.compose.material.Button
|
|
|
|
|
import androidx.compose.material.ButtonDefaults
|
|
|
|
|
import androidx.compose.material.Card
|
|
|
|
|
import androidx.compose.material.DropdownMenu
|
|
|
|
|
import androidx.compose.material.DropdownMenuItem
|
|
|
|
@ -50,6 +53,7 @@ import androidx.compose.ui.platform.LocalDensity
|
|
|
|
|
import androidx.compose.ui.res.colorResource
|
|
|
|
|
import androidx.compose.ui.res.painterResource
|
|
|
|
|
import androidx.compose.ui.res.stringResource
|
|
|
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
|
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
|
import androidx.compose.ui.unit.sp
|
|
|
|
@ -59,6 +63,7 @@ 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 org.mozilla.fenix.FeatureFlags
|
|
|
|
|
import org.mozilla.fenix.R
|
|
|
|
|
import org.mozilla.fenix.components.components
|
|
|
|
|
import org.mozilla.fenix.compose.Image
|
|
|
|
@ -71,14 +76,19 @@ import org.mozilla.fenix.theme.FirefoxTheme
|
|
|
|
|
* @param recentTabs List of [RecentTab] to display.
|
|
|
|
|
* @param menuItems List of [RecentTabMenuItem] shown long clicking a [RecentTab].
|
|
|
|
|
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
|
|
|
|
|
* @param onRecentSearchGroupClicked Invoked when the user clicks on a recent search group.
|
|
|
|
|
* @param onRecentSearchGroupClick Invoked when the user clicks on a recent search group.
|
|
|
|
|
* @param onRecentSyncedTabClick Invoked when the user clicks on the recent synced tab.
|
|
|
|
|
* @param onSyncedTabSeeAllButtonClick Invoked when user clicks on the "See all" button in the synced tab card.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
@Suppress("LongParameterList")
|
|
|
|
|
fun RecentTabs(
|
|
|
|
|
recentTabs: List<RecentTab>,
|
|
|
|
|
menuItems: List<RecentTabMenuItem>,
|
|
|
|
|
onRecentTabClick: (String) -> Unit = {},
|
|
|
|
|
onRecentSearchGroupClicked: (String) -> Unit = {}
|
|
|
|
|
onRecentSearchGroupClick: (String) -> Unit = {},
|
|
|
|
|
onRecentSyncedTabClick: (RecentTab.SyncedTab) -> Unit = {},
|
|
|
|
|
onSyncedTabSeeAllButtonClick: () -> Unit = {},
|
|
|
|
|
) {
|
|
|
|
|
Column(
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
@ -99,7 +109,16 @@ fun RecentTabs(
|
|
|
|
|
searchTerm = tab.searchTerm,
|
|
|
|
|
tabId = tab.tabId,
|
|
|
|
|
count = tab.count,
|
|
|
|
|
onSearchGroupClicked = onRecentSearchGroupClicked
|
|
|
|
|
onSearchGroupClick = onRecentSearchGroupClick
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
is RecentTab.SyncedTab -> {
|
|
|
|
|
if (FeatureFlags.taskContinuityFeature) {
|
|
|
|
|
RecentSyncedTabItem(
|
|
|
|
|
tab,
|
|
|
|
|
onRecentSyncedTabClick,
|
|
|
|
|
onSyncedTabSeeAllButtonClick,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -158,9 +177,7 @@ private fun RecentTabItem(
|
|
|
|
|
Row {
|
|
|
|
|
RecentTabIcon(
|
|
|
|
|
url = tab.state.content.url,
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.size(18.dp, 18.dp)
|
|
|
|
|
.clip(RoundedCornerShape(2.dp)),
|
|
|
|
|
modifier = Modifier.size(18.dp).clip(RoundedCornerShape(2.dp)),
|
|
|
|
|
icon = tab.state.content.icon
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
@ -186,7 +203,7 @@ private fun RecentTabItem(
|
|
|
|
|
* @param searchTerm The search term for the group.
|
|
|
|
|
* @param tabId The id of the last accessed tab in the group.
|
|
|
|
|
* @param count Count of how many tabs belongs to the group.
|
|
|
|
|
* @param onSearchGroupClicked Invoked when the user clicks on a group.
|
|
|
|
|
* @param onSearchGroupClick Invoked when the user clicks on a group.
|
|
|
|
|
*/
|
|
|
|
|
@Suppress("LongParameterList")
|
|
|
|
|
@Composable
|
|
|
|
@ -194,13 +211,13 @@ private fun RecentSearchGroupItem(
|
|
|
|
|
searchTerm: String,
|
|
|
|
|
tabId: String,
|
|
|
|
|
count: Int,
|
|
|
|
|
onSearchGroupClicked: (String) -> Unit = {}
|
|
|
|
|
onSearchGroupClick: (String) -> Unit = {}
|
|
|
|
|
) {
|
|
|
|
|
Card(
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
.height(112.dp)
|
|
|
|
|
.clickable { onSearchGroupClicked(tabId) },
|
|
|
|
|
.clickable { onSearchGroupClick(tabId) },
|
|
|
|
|
shape = RoundedCornerShape(8.dp),
|
|
|
|
|
backgroundColor = FirefoxTheme.colors.layer2,
|
|
|
|
|
elevation = 6.dp
|
|
|
|
@ -244,25 +261,139 @@ private fun RecentSearchGroupItem(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A recent synced tab.
|
|
|
|
|
*
|
|
|
|
|
* @param tab Optional synced tab. If null, displays placeholders.
|
|
|
|
|
* @param onRecentSyncedTabClick Invoked when item is clicked.
|
|
|
|
|
* @param onSeeAllButtonClick Invoked when "See all" button is clicked.
|
|
|
|
|
*/
|
|
|
|
|
@Suppress("LongMethod")
|
|
|
|
|
@Composable
|
|
|
|
|
private fun RecentSyncedTabItem(
|
|
|
|
|
tab: RecentTab.SyncedTab?,
|
|
|
|
|
onRecentSyncedTabClick: (RecentTab.SyncedTab) -> Unit,
|
|
|
|
|
onSeeAllButtonClick: () -> Unit,
|
|
|
|
|
) {
|
|
|
|
|
Card(
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
.height(180.dp)
|
|
|
|
|
.clickable { tab?.let { onRecentSyncedTabClick(tab) } },
|
|
|
|
|
shape = RoundedCornerShape(8.dp),
|
|
|
|
|
backgroundColor = FirefoxTheme.colors.layer2,
|
|
|
|
|
elevation = 6.dp
|
|
|
|
|
) {
|
|
|
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
|
|
|
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
|
|
|
|
if (tab == null) {
|
|
|
|
|
RecentTabImagePlaceholder()
|
|
|
|
|
} else {
|
|
|
|
|
RecentTabImage(
|
|
|
|
|
tab = tab,
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.size(108.dp, 80.dp)
|
|
|
|
|
.clip(RoundedCornerShape(8.dp))
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
|
|
|
|
|
|
Column(
|
|
|
|
|
verticalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
modifier = Modifier.fillMaxHeight()
|
|
|
|
|
) {
|
|
|
|
|
if (tab == null) {
|
|
|
|
|
RecentTabTitlePlaceholder()
|
|
|
|
|
} else {
|
|
|
|
|
RecentTabTitle(title = tab.title)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
|
if (tab == null) {
|
|
|
|
|
Box(
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.background(FirefoxTheme.colors.layer3)
|
|
|
|
|
.size(18.dp)
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
Image(
|
|
|
|
|
painter = painterResource(R.drawable.ic_synced_tabs),
|
|
|
|
|
contentDescription = stringResource(
|
|
|
|
|
R.string.recent_tabs_synced_device_icon_content_description
|
|
|
|
|
),
|
|
|
|
|
modifier = Modifier.size(18.dp, 18.dp)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
|
|
|
|
|
|
|
|
if (tab == null) {
|
|
|
|
|
TextLinePlaceHolder()
|
|
|
|
|
} else {
|
|
|
|
|
RecentTabSubtitle(subtitle = tab.deviceDisplayName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(32.dp))
|
|
|
|
|
|
|
|
|
|
Button(
|
|
|
|
|
onClick = onSeeAllButtonClick,
|
|
|
|
|
colors = ButtonDefaults.outlinedButtonColors(
|
|
|
|
|
backgroundColor = if (tab == null) {
|
|
|
|
|
FirefoxTheme.colors.layer3
|
|
|
|
|
} else {
|
|
|
|
|
FirefoxTheme.colors.actionSecondary
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
elevation = ButtonDefaults.elevation(
|
|
|
|
|
defaultElevation = 0.dp,
|
|
|
|
|
pressedElevation = 0.dp
|
|
|
|
|
),
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.height(36.dp)
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
) {
|
|
|
|
|
if (tab != null) {
|
|
|
|
|
Text(
|
|
|
|
|
text = stringResource(R.string.recent_tabs_see_all_synced_tabs_button_text),
|
|
|
|
|
textAlign = TextAlign.Center,
|
|
|
|
|
color = FirefoxTheme.colors.textActionSecondary
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A recent tab image.
|
|
|
|
|
*
|
|
|
|
|
* @param tab [RecentTab.Tab] that was recently viewed.
|
|
|
|
|
* @param tab [RecentTab] that was recently viewed.
|
|
|
|
|
* @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.
|
|
|
|
|
* is null.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
@Suppress("LongParameterList")
|
|
|
|
|
private fun RecentTabImage(
|
|
|
|
|
tab: RecentTab.Tab,
|
|
|
|
|
tab: RecentTab,
|
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
|
contentScale: ContentScale = ContentScale.FillWidth,
|
|
|
|
|
alignment: Alignment = Alignment.TopCenter
|
|
|
|
|
) {
|
|
|
|
|
val previewImageUrl = tab.state.content.previewImageUrl
|
|
|
|
|
val thumbnail = tab.state.content.thumbnail
|
|
|
|
|
val (previewImageUrl, loaderUrl, key) = when (tab) {
|
|
|
|
|
is RecentTab.Tab -> Triple(
|
|
|
|
|
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 {
|
|
|
|
|
!previewImageUrl.isNullOrEmpty() -> {
|
|
|
|
@ -287,15 +418,10 @@ private fun RecentTabImage(
|
|
|
|
|
modifier = modifier,
|
|
|
|
|
backgroundColor = colorResource(id = R.color.photonGrey20)
|
|
|
|
|
) {
|
|
|
|
|
components.core.icons.Loader(tab.state.content.url) {
|
|
|
|
|
components.core.icons.Loader(loaderUrl) {
|
|
|
|
|
Placeholder {
|
|
|
|
|
Box(
|
|
|
|
|
modifier = Modifier.background(
|
|
|
|
|
color = when (isSystemInDarkTheme()) {
|
|
|
|
|
true -> PhotonColors.DarkGrey30
|
|
|
|
|
false -> PhotonColors.LightGrey30
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
modifier = Modifier.background(color = FirefoxTheme.colors.layer3)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -317,7 +443,7 @@ private fun RecentTabImage(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ThumbnailImage(
|
|
|
|
|
tabId = tab.state.id,
|
|
|
|
|
key = key,
|
|
|
|
|
modifier = modifier,
|
|
|
|
|
contentScale = contentScale,
|
|
|
|
|
alignment = alignment
|
|
|
|
@ -327,6 +453,19 @@ private fun RecentTabImage(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A placeholder for a recent tab image.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
private fun RecentTabImagePlaceholder() {
|
|
|
|
|
Box(
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.size(108.dp, 80.dp)
|
|
|
|
|
.clip(RoundedCornerShape(8.dp))
|
|
|
|
|
.background(color = FirefoxTheme.colors.layer3)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Menu shown for a [RecentTab.Tab].
|
|
|
|
|
*
|
|
|
|
@ -444,6 +583,20 @@ private fun RecentTabTitle(title: String) {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A placeholder for a tab title.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
private fun RecentTabTitlePlaceholder() {
|
|
|
|
|
Column {
|
|
|
|
|
TextLinePlaceHolder()
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
|
|
|
|
|
|
TextLinePlaceHolder()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A recent tab subtitle.
|
|
|
|
|
*
|
|
|
|
@ -465,18 +618,18 @@ private fun RecentTabSubtitle(subtitle: String) {
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
private fun ThumbnailImage(
|
|
|
|
|
tabId: String,
|
|
|
|
|
key: String,
|
|
|
|
|
modifier: Modifier,
|
|
|
|
|
contentScale: ContentScale,
|
|
|
|
|
alignment: Alignment
|
|
|
|
|
) {
|
|
|
|
|
val rememberBitmap = remember(tabId) { mutableStateOf<ImageBitmap?>(null) }
|
|
|
|
|
val rememberBitmap = remember(key) { mutableStateOf<ImageBitmap?>(null) }
|
|
|
|
|
val size = LocalDensity.current.run { 108.dp.toPx().toInt() }
|
|
|
|
|
val request = ImageLoadRequest(tabId, size)
|
|
|
|
|
val request = ImageLoadRequest(key, size)
|
|
|
|
|
val storage = components.core.thumbnailStorage
|
|
|
|
|
val bitmap = rememberBitmap.value
|
|
|
|
|
|
|
|
|
|
LaunchedEffect(tabId) {
|
|
|
|
|
LaunchedEffect(key) {
|
|
|
|
|
rememberBitmap.value = storage.loadThumbnail(request).await()?.asImageBitmap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -491,3 +644,13 @@ private fun ThumbnailImage(
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
private fun TextLinePlaceHolder() {
|
|
|
|
|
Box(
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
.height(12.dp)
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
.background(FirefoxTheme.colors.layer3)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|