1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.home.recentvisits.view
7 import androidx.compose.foundation.ExperimentalFoundationApi
8 import androidx.compose.foundation.Image
9 import androidx.compose.foundation.background
10 import androidx.compose.foundation.combinedClickable
11 import androidx.compose.foundation.isSystemInDarkTheme
12 import androidx.compose.foundation.layout.Arrangement
13 import androidx.compose.foundation.layout.Box
14 import androidx.compose.foundation.layout.Column
15 import androidx.compose.foundation.layout.PaddingValues
16 import androidx.compose.foundation.layout.Row
17 import androidx.compose.foundation.layout.Spacer
18 import androidx.compose.foundation.layout.fillMaxHeight
19 import androidx.compose.foundation.layout.fillMaxSize
20 import androidx.compose.foundation.layout.fillMaxWidth
21 import androidx.compose.foundation.layout.padding
22 import androidx.compose.foundation.layout.size
23 import androidx.compose.foundation.layout.width
24 import androidx.compose.foundation.lazy.LazyListState
25 import androidx.compose.foundation.lazy.LazyRow
26 import androidx.compose.foundation.lazy.itemsIndexed
27 import androidx.compose.foundation.lazy.rememberLazyListState
28 import androidx.compose.foundation.shape.RoundedCornerShape
29 import androidx.compose.material.Card
30 import androidx.compose.material.Divider
31 import androidx.compose.material.DropdownMenu
32 import androidx.compose.material.DropdownMenuItem
33 import androidx.compose.material.Text
34 import androidx.compose.runtime.Composable
35 import androidx.compose.runtime.DisposableEffect
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.mutableStateOf
38 import androidx.compose.runtime.remember
39 import androidx.compose.runtime.setValue
40 import androidx.compose.ui.Alignment
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.graphics.Color
43 import androidx.compose.ui.platform.LocalConfiguration
44 import androidx.compose.ui.platform.LocalContext
45 import androidx.compose.ui.res.painterResource
46 import androidx.compose.ui.text.style.TextOverflow
47 import androidx.compose.ui.tooling.preview.Preview
48 import androidx.compose.ui.unit.dp
49 import androidx.compose.ui.unit.sp
50 import mozilla.components.support.ktx.kotlin.trimmed
51 import org.mozilla.fenix.R
52 import org.mozilla.fenix.compose.EagerFlingBehavior
53 import org.mozilla.fenix.compose.Favicon
54 import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
55 import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
56 import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
57 import org.mozilla.fenix.theme.FirefoxTheme
58 import org.mozilla.fenix.theme.Theme
60 // Number of recently visited items per column.
61 private const val VISITS_PER_COLUMN = 3
64 * A list of recently visited items.
66 * @param recentVisits List of [RecentlyVisitedItem] to display.
67 * @param menuItems List of [RecentVisitMenuItem] shown long clicking a [RecentlyVisitedItem].
68 * @param backgroundColor The background [Color] of each item.
69 * @param onRecentVisitClick Invoked when the user clicks on a recent visit.
70 * @param onRecentVisitLongClick Invoked when the user long clicks on a recent visit.
74 recentVisits: List<RecentlyVisitedItem>,
75 menuItems: List<RecentVisitMenuItem>,
76 backgroundColor: Color = FirefoxTheme.colors.layer2,
77 onRecentVisitClick: (RecentlyVisitedItem, Int) -> Unit = { _, _ -> },
78 onRecentVisitLongClick: () -> Unit = {},
81 modifier = Modifier.fillMaxWidth(),
82 shape = RoundedCornerShape(8.dp),
83 backgroundColor = backgroundColor,
86 val listState = rememberLazyListState()
87 val flingBehavior = EagerFlingBehavior(lazyRowState = listState)
91 contentPadding = PaddingValues(16.dp),
92 horizontalArrangement = Arrangement.spacedBy(32.dp),
93 flingBehavior = flingBehavior,
95 val itemsList = recentVisits.chunked(VISITS_PER_COLUMN)
97 itemsIndexed(itemsList) { pageIndex, items ->
99 modifier = Modifier.fillMaxWidth(),
101 items.forEachIndexed { index, recentVisit ->
103 is RecentHistoryHighlight -> RecentlyVisitedHistoryHighlight(
104 recentVisit = recentVisit,
105 menuItems = menuItems,
106 clickableEnabled = listState.atLeastHalfVisibleItems.contains(pageIndex),
107 showDividerLine = index < items.size - 1,
108 onRecentVisitClick = {
109 onRecentVisitClick(it, pageIndex + 1)
111 onRecentVisitLongClick = { onRecentVisitLongClick() },
113 is RecentHistoryGroup -> RecentlyVisitedHistoryGroup(
114 recentVisit = recentVisit,
115 menuItems = menuItems,
116 clickableEnabled = listState.atLeastHalfVisibleItems.contains(pageIndex),
117 showDividerLine = index < items.size - 1,
118 onRecentVisitClick = {
119 onRecentVisitClick(it, pageIndex + 1)
121 onRecentVisitLongClick = { onRecentVisitLongClick() },
132 * A recently visited history group.
134 * @param recentVisit The [RecentHistoryGroup] to display.
135 * @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
136 * @param clickableEnabled Whether click actions should be invoked or not.
137 * @param showDividerLine Whether to show a divider line at the bottom.
138 * @param onRecentVisitClick Invoked when the user clicks on a recent visit.
139 * @param onRecentVisitClick Invoked when the user long clicks on a recently visited group.
141 @OptIn(ExperimentalFoundationApi::class)
142 @Suppress("LongParameterList")
144 private fun RecentlyVisitedHistoryGroup(
145 recentVisit: RecentHistoryGroup,
146 menuItems: List<RecentVisitMenuItem>,
147 clickableEnabled: Boolean,
148 showDividerLine: Boolean,
149 onRecentVisitClick: (RecentHistoryGroup) -> Unit = { _ -> },
150 onRecentVisitLongClick: () -> Unit = {},
152 var isMenuExpanded by remember { mutableStateOf(false) }
157 enabled = clickableEnabled,
158 onClick = { onRecentVisitClick(recentVisit) },
160 onRecentVisitLongClick()
161 isMenuExpanded = true
164 .size(268.dp, 56.dp),
165 verticalAlignment = Alignment.CenterVertically,
168 painter = painterResource(R.drawable.ic_multiple_tabs),
169 contentDescription = null,
170 modifier = Modifier.size(24.dp),
173 Spacer(modifier = Modifier.width(16.dp))
176 modifier = Modifier.fillMaxSize(),
178 RecentlyVisitedTitle(
179 text = recentVisit.title,
181 .padding(top = 7.dp, bottom = 2.dp)
185 RecentlyVisitedCaption(
186 count = recentVisit.historyMetadata.size,
187 modifier = Modifier.weight(1f),
190 if (showDividerLine) {
191 RecentlyVisitedDivider()
196 showMenu = isMenuExpanded,
197 menuItems = menuItems,
198 recentVisit = recentVisit,
199 onDismissRequest = { isMenuExpanded = false },
205 * A recently visited history item.
207 * @param recentVisit The [RecentHistoryHighlight] to display.
208 * @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
209 * @param clickableEnabled Whether click actions should be invoked or not.
210 * @param showDividerLine Whether to show a divider line at the bottom.
211 * @param onRecentVisitClick Invoked when the user clicks on a recent visit.
212 * @param onRecentVisitLongClick Invoked when the user long clicks on a recent visit highlight.
214 @OptIn(ExperimentalFoundationApi::class)
215 @Suppress("LongParameterList")
217 private fun RecentlyVisitedHistoryHighlight(
218 recentVisit: RecentHistoryHighlight,
219 menuItems: List<RecentVisitMenuItem>,
220 clickableEnabled: Boolean,
221 showDividerLine: Boolean,
222 onRecentVisitClick: (RecentHistoryHighlight) -> Unit = { _ -> },
223 onRecentVisitLongClick: () -> Unit = {},
225 var isMenuExpanded by remember { mutableStateOf(false) }
230 enabled = clickableEnabled,
231 onClick = { onRecentVisitClick(recentVisit) },
233 onRecentVisitLongClick()
234 isMenuExpanded = true
237 .size(268.dp, 56.dp),
238 verticalAlignment = Alignment.CenterVertically,
240 Favicon(url = recentVisit.url, size = 24.dp)
242 Spacer(modifier = Modifier.width(16.dp))
244 Box(modifier = Modifier.fillMaxSize()) {
245 RecentlyVisitedTitle(
246 text = recentVisit.title.trimmed(),
247 modifier = Modifier.align(Alignment.CenterStart),
250 if (showDividerLine) {
251 RecentlyVisitedDivider(modifier = Modifier.align(Alignment.BottomCenter))
256 showMenu = isMenuExpanded,
257 menuItems = menuItems,
258 recentVisit = recentVisit,
259 onDismissRequest = { isMenuExpanded = false },
265 * The title of a recent visit.
267 * @param text [String] that will be display. Will be ellipsized if cannot fit on one line.
268 * @param modifier [Modifier] allowing to perfectly place this.
271 private fun RecentlyVisitedTitle(
273 modifier: Modifier = Modifier,
278 color = FirefoxTheme.colors.textPrimary,
280 overflow = TextOverflow.Ellipsis,
286 * The caption text for a recent visit.
288 * @param count Number of recently visited items to display in the caption.
289 * @param modifier [Modifier] allowing to perfectly place this.
292 private fun RecentlyVisitedCaption(
296 val stringId = if (count == 1) {
297 R.string.history_search_group_site
299 R.string.history_search_group_sites
303 text = String.format(LocalContext.current.getString(stringId), count),
305 color = when (isSystemInDarkTheme()) {
306 true -> FirefoxTheme.colors.textPrimary
307 false -> FirefoxTheme.colors.textSecondary
310 overflow = TextOverflow.Ellipsis,
316 * Menu shown for a [RecentlyVisitedItem].
318 * @see [DropdownMenu]
320 * @param showMenu Whether this is currently open and visible to the user.
321 * @param menuItems List of options shown.
322 * @param recentVisit The [RecentlyVisitedItem] for which this menu is shown.
323 * @param onDismissRequest Called when the user chooses a menu option or requests to dismiss the menu.
326 private fun RecentlyVisitedMenu(
328 menuItems: List<RecentVisitMenuItem>,
329 recentVisit: RecentlyVisitedItem,
330 onDismissRequest: () -> Unit,
332 DisposableEffect(LocalConfiguration.current.orientation) {
333 onDispose { onDismissRequest() }
338 onDismissRequest = { onDismissRequest() },
340 .background(color = FirefoxTheme.colors.layer2),
342 for (item in menuItems) {
346 item.onClick(recentVisit)
351 color = FirefoxTheme.colors.textPrimary,
355 .align(Alignment.CenterVertically),
363 * A recent item divider.
365 * @param modifier [Modifier] allowing to perfectly place this.
368 private fun RecentlyVisitedDivider(
369 modifier: Modifier = Modifier,
373 color = FirefoxTheme.colors.borderPrimary,
379 * Get the indexes in list of all items which have more than half showing.
381 private val LazyListState.atLeastHalfVisibleItems
385 val startEdge = maxOf(0, layoutInfo.viewportStartOffset - it.offset)
386 val endEdge = maxOf(0, it.offset + it.size - layoutInfo.viewportEndOffset)
387 return@filter startEdge + endEdge < it.size / 2
392 private fun RecentlyVisitedPreview() {
393 FirefoxTheme(theme = Theme.getTheme()) {
395 recentVisits = listOf(
396 RecentHistoryGroup(title = "running shoes"),
397 RecentHistoryGroup(title = "mozilla"),
398 RecentHistoryGroup(title = "firefox"),
399 RecentHistoryGroup(title = "pocket"),
401 menuItems = emptyList(),