[fenix] For https://github.com/mozilla-mobile/fenix/issues/26520 - Color homepage...
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / home / recentvisits / view / RecentlyVisited.kt
blob954c2cb1b2fa64762172d44b0a9365b73fc877e3
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
63 /**
64  * A list of recently visited items.
65  *
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.
71  */
72 @Composable
73 fun RecentlyVisited(
74     recentVisits: List<RecentlyVisitedItem>,
75     menuItems: List<RecentVisitMenuItem>,
76     backgroundColor: Color = FirefoxTheme.colors.layer2,
77     onRecentVisitClick: (RecentlyVisitedItem, Int) -> Unit = { _, _ -> },
78     onRecentVisitLongClick: () -> Unit = {},
79 ) {
80     Card(
81         modifier = Modifier.fillMaxWidth(),
82         shape = RoundedCornerShape(8.dp),
83         backgroundColor = backgroundColor,
84         elevation = 6.dp,
85     ) {
86         val listState = rememberLazyListState()
87         val flingBehavior = EagerFlingBehavior(lazyRowState = listState)
89         LazyRow(
90             state = listState,
91             contentPadding = PaddingValues(16.dp),
92             horizontalArrangement = Arrangement.spacedBy(32.dp),
93             flingBehavior = flingBehavior,
94         ) {
95             val itemsList = recentVisits.chunked(VISITS_PER_COLUMN)
97             itemsIndexed(itemsList) { pageIndex, items ->
98                 Column(
99                     modifier = Modifier.fillMaxWidth(),
100                 ) {
101                     items.forEachIndexed { index, recentVisit ->
102                         when (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)
110                                 },
111                                 onRecentVisitLongClick = { onRecentVisitLongClick() },
112                             )
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)
120                                 },
121                                 onRecentVisitLongClick = { onRecentVisitLongClick() },
122                             )
123                         }
124                     }
125                 }
126             }
127         }
128     }
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.
140  */
141 @OptIn(ExperimentalFoundationApi::class)
142 @Suppress("LongParameterList")
143 @Composable
144 private fun RecentlyVisitedHistoryGroup(
145     recentVisit: RecentHistoryGroup,
146     menuItems: List<RecentVisitMenuItem>,
147     clickableEnabled: Boolean,
148     showDividerLine: Boolean,
149     onRecentVisitClick: (RecentHistoryGroup) -> Unit = { _ -> },
150     onRecentVisitLongClick: () -> Unit = {},
151 ) {
152     var isMenuExpanded by remember { mutableStateOf(false) }
154     Row(
155         modifier = Modifier
156             .combinedClickable(
157                 enabled = clickableEnabled,
158                 onClick = { onRecentVisitClick(recentVisit) },
159                 onLongClick = {
160                     onRecentVisitLongClick()
161                     isMenuExpanded = true
162                 },
163             )
164             .size(268.dp, 56.dp),
165         verticalAlignment = Alignment.CenterVertically,
166     ) {
167         Image(
168             painter = painterResource(R.drawable.ic_multiple_tabs),
169             contentDescription = null,
170             modifier = Modifier.size(24.dp),
171         )
173         Spacer(modifier = Modifier.width(16.dp))
175         Column(
176             modifier = Modifier.fillMaxSize(),
177         ) {
178             RecentlyVisitedTitle(
179                 text = recentVisit.title,
180                 modifier = Modifier
181                     .padding(top = 7.dp, bottom = 2.dp)
182                     .weight(1f),
183             )
185             RecentlyVisitedCaption(
186                 count = recentVisit.historyMetadata.size,
187                 modifier = Modifier.weight(1f),
188             )
190             if (showDividerLine) {
191                 RecentlyVisitedDivider()
192             }
193         }
195         RecentlyVisitedMenu(
196             showMenu = isMenuExpanded,
197             menuItems = menuItems,
198             recentVisit = recentVisit,
199             onDismissRequest = { isMenuExpanded = false },
200         )
201     }
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.
213  */
214 @OptIn(ExperimentalFoundationApi::class)
215 @Suppress("LongParameterList")
216 @Composable
217 private fun RecentlyVisitedHistoryHighlight(
218     recentVisit: RecentHistoryHighlight,
219     menuItems: List<RecentVisitMenuItem>,
220     clickableEnabled: Boolean,
221     showDividerLine: Boolean,
222     onRecentVisitClick: (RecentHistoryHighlight) -> Unit = { _ -> },
223     onRecentVisitLongClick: () -> Unit = {},
224 ) {
225     var isMenuExpanded by remember { mutableStateOf(false) }
227     Row(
228         modifier = Modifier
229             .combinedClickable(
230                 enabled = clickableEnabled,
231                 onClick = { onRecentVisitClick(recentVisit) },
232                 onLongClick = {
233                     onRecentVisitLongClick()
234                     isMenuExpanded = true
235                 },
236             )
237             .size(268.dp, 56.dp),
238         verticalAlignment = Alignment.CenterVertically,
239     ) {
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),
248             )
250             if (showDividerLine) {
251                 RecentlyVisitedDivider(modifier = Modifier.align(Alignment.BottomCenter))
252             }
253         }
255         RecentlyVisitedMenu(
256             showMenu = isMenuExpanded,
257             menuItems = menuItems,
258             recentVisit = recentVisit,
259             onDismissRequest = { isMenuExpanded = false },
260         )
261     }
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.
269  */
270 @Composable
271 private fun RecentlyVisitedTitle(
272     text: String,
273     modifier: Modifier = Modifier,
274 ) {
275     Text(
276         text = text,
277         modifier = modifier,
278         color = FirefoxTheme.colors.textPrimary,
279         fontSize = 16.sp,
280         overflow = TextOverflow.Ellipsis,
281         maxLines = 1,
282     )
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.
290  */
291 @Composable
292 private fun RecentlyVisitedCaption(
293     count: Int,
294     modifier: Modifier,
295 ) {
296     val stringId = if (count == 1) {
297         R.string.history_search_group_site
298     } else {
299         R.string.history_search_group_sites
300     }
302     Text(
303         text = String.format(LocalContext.current.getString(stringId), count),
304         modifier = modifier,
305         color = when (isSystemInDarkTheme()) {
306             true -> FirefoxTheme.colors.textPrimary
307             false -> FirefoxTheme.colors.textSecondary
308         },
309         fontSize = 12.sp,
310         overflow = TextOverflow.Ellipsis,
311         maxLines = 1,
312     )
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.
324  */
325 @Composable
326 private fun RecentlyVisitedMenu(
327     showMenu: Boolean,
328     menuItems: List<RecentVisitMenuItem>,
329     recentVisit: RecentlyVisitedItem,
330     onDismissRequest: () -> Unit,
331 ) {
332     DisposableEffect(LocalConfiguration.current.orientation) {
333         onDispose { onDismissRequest() }
334     }
336     DropdownMenu(
337         expanded = showMenu,
338         onDismissRequest = { onDismissRequest() },
339         modifier = Modifier
340             .background(color = FirefoxTheme.colors.layer2),
341     ) {
342         for (item in menuItems) {
343             DropdownMenuItem(
344                 onClick = {
345                     onDismissRequest()
346                     item.onClick(recentVisit)
347                 },
348             ) {
349                 Text(
350                     text = item.title,
351                     color = FirefoxTheme.colors.textPrimary,
352                     maxLines = 1,
353                     modifier = Modifier
354                         .fillMaxHeight()
355                         .align(Alignment.CenterVertically),
356                 )
357             }
358         }
359     }
363  * A recent item divider.
365  * @param modifier [Modifier] allowing to perfectly place this.
366  */
367 @Composable
368 private fun RecentlyVisitedDivider(
369     modifier: Modifier = Modifier,
370 ) {
371     Divider(
372         modifier = modifier,
373         color = FirefoxTheme.colors.borderPrimary,
374         thickness = 0.5.dp,
375     )
379  * Get the indexes in list of all items which have more than half showing.
380  */
381 private val LazyListState.atLeastHalfVisibleItems
382     get() = layoutInfo
383         .visibleItemsInfo
384         .filter {
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
388         }.map { it.index }
390 @Composable
391 @Preview
392 private fun RecentlyVisitedPreview() {
393     FirefoxTheme(theme = Theme.getTheme()) {
394         RecentlyVisited(
395             recentVisits = listOf(
396                 RecentHistoryGroup(title = "running shoes"),
397                 RecentHistoryGroup(title = "mozilla"),
398                 RecentHistoryGroup(title = "firefox"),
399                 RecentHistoryGroup(title = "pocket"),
400             ),
401             menuItems = emptyList(),
402         )
403     }