You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt

304 lines
13 KiB
Kotlin

/* 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.tabstray
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.tabstray.ext.isNormalTab
import org.mozilla.fenix.tabstray.inactivetabs.InactiveTabsList
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Top-level UI for displaying the Tabs Tray feature.
*
* @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState].
* @param displayTabsInGrid Whether the normal and private tabs should be displayed in a grid.
* @param onTabClose Invoked when the user clicks to close a tab.
* @param onTabMediaClick Invoked when the user interacts with a tab's media controls.
* @param onTabClick Invoked when the user clicks on a tab.
* @param onTabMultiSelectClick Invoked when the user clicks on a tab while in multi-select mode.
* @param onTabLongClick Invoked when the user long clicks a tab.
* @param onInactiveTabsHeaderClick Invoked when the user clicks on the inactive tabs section header.
* @param onDeleteAllInactiveTabsClick Invoked when the user clicks on the delete all inactive tabs button.
* @param onInactiveTabsAutoCloseDialogShown Invoked when the inactive tabs auto close dialog
* is presented to the user.
* @param onInactiveTabAutoCloseDialogCloseButtonClick Invoked when the user clicks on the inactive
* tab auto close dialog's dismiss button.
* @param onEnableInactiveTabAutoCloseClick Invoked when the user clicks on the inactive tab auto
* close dialog's enable button.
* @param onInactiveTabClick Invoked when the user clicks on an inactive tab.
* @param onInactiveTabClose Invoked when the user clicks on an inactive tab's close button.
*/
@OptIn(ExperimentalPagerApi::class, ExperimentalComposeUiApi::class)
@Suppress("LongMethod", "LongParameterList")
@Composable
fun TabsTray(
appStore: AppStore,
tabsTrayStore: TabsTrayStore,
displayTabsInGrid: Boolean,
shouldShowInactiveTabsAutoCloseDialog: (Int) -> Boolean,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
onTabMultiSelectClick: (TabSessionState) -> Unit,
onTabLongClick: (TabSessionState) -> Unit,
onInactiveTabsHeaderClick: (Boolean) -> Unit,
onDeleteAllInactiveTabsClick: () -> Unit,
onInactiveTabsAutoCloseDialogShown: () -> Unit,
onInactiveTabAutoCloseDialogCloseButtonClick: () -> Unit,
onEnableInactiveTabAutoCloseClick: () -> Unit,
onInactiveTabClick: (TabSessionState) -> Unit,
onInactiveTabClose: (TabSessionState) -> Unit,
) {
val multiselectMode = tabsTrayStore
.observeAsComposableState { state -> state.mode }.value ?: TabsTrayState.Mode.Normal
val normalTabs = tabsTrayStore
.observeAsComposableState { state -> state.normalTabs }.value ?: emptyList()
val privateTabs = tabsTrayStore
.observeAsComposableState { state -> state.privateTabs }.value ?: emptyList()
val inactiveTabsExpanded = appStore
.observeAsComposableState { state -> state.inactiveTabsExpanded }.value ?: false
val inactiveTabs = tabsTrayStore
.observeAsComposableState { state -> state.inactiveTabs }.value ?: emptyList()
val pagerState = rememberPagerState(initialPage = 0)
val scope = rememberCoroutineScope()
val isInMultiSelectMode = multiselectMode is TabsTrayState.Mode.Select
val animateScrollToPage: ((Page) -> Unit) = { page ->
scope.launch {
pagerState.animateScrollToPage(page.ordinal)
}
}
val handleTabClick: ((TabSessionState) -> Unit) = { tab ->
if (isInMultiSelectMode) {
onTabMultiSelectClick(tab)
} else {
onTabClick(tab)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layer1),
) {
Box(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
TabsTrayBanner(
isInMultiSelectMode = isInMultiSelectMode,
onTabPageIndicatorClicked = animateScrollToPage,
)
}
Divider()
Box(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
count = Page.values().size,
modifier = Modifier.fillMaxSize(),
state = pagerState,
userScrollEnabled = false,
) { position ->
when (Page.positionToPage(position)) {
Page.NormalTabs -> {
val showInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog(inactiveTabs.size)
var showAutoCloseDialog by remember { mutableStateOf(showInactiveTabsAutoCloseDialog) }
val optionalInactiveTabsHeader: (@Composable () -> Unit)? = if (inactiveTabs.isEmpty()) {
null
} else {
{
InactiveTabsList(
inactiveTabs = inactiveTabs,
expanded = inactiveTabsExpanded,
showAutoCloseDialog = showAutoCloseDialog,
onHeaderClick = onInactiveTabsHeaderClick,
onDeleteAllButtonClick = onDeleteAllInactiveTabsClick,
onAutoCloseDismissClick = {
onInactiveTabAutoCloseDialogCloseButtonClick()
showAutoCloseDialog = !showAutoCloseDialog
},
onEnableAutoCloseClick = {
onEnableInactiveTabAutoCloseClick()
showAutoCloseDialog = !showAutoCloseDialog
},
onTabClick = onInactiveTabClick,
onTabCloseClick = onInactiveTabClose,
)
}
}
if (showInactiveTabsAutoCloseDialog) {
onInactiveTabsAutoCloseDialogShown()
}
TabLayout(
tabs = normalTabs,
displayTabsInGrid = displayTabsInGrid,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
header = optionalInactiveTabsHeader,
)
}
Page.PrivateTabs -> {
TabLayout(
tabs = privateTabs,
displayTabsInGrid = displayTabsInGrid,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
)
}
Page.SyncedTabs -> {
Text(
text = "Synced tabs",
modifier = Modifier.padding(all = 16.dp),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.body1,
)
}
}
}
}
}
}
@LightDarkPreview
@Composable
private fun TabsTrayPreview() {
TabsTrayPreviewRoot(
displayTabsInGrid = false,
normalTabs = generateFakeTabsList(),
privateTabs = generateFakeTabsList(
tabCount = 7,
isPrivate = true,
),
)
}
@LightDarkPreview
@Composable
private fun TabsTrayMultiSelectPreview() {
TabsTrayPreviewRoot(
mode = TabsTrayState.Mode.Select(setOf()),
normalTabs = generateFakeTabsList(),
)
}
@LightDarkPreview
@Composable
private fun TabsTrayInactiveTabsPreview() {
TabsTrayPreviewRoot(
normalTabs = generateFakeTabsList(tabCount = 3),
inactiveTabs = generateFakeTabsList(),
inactiveTabsExpanded = true,
showInactiveTabsAutoCloseDialog = true,
)
}
@Composable
private fun TabsTrayPreviewRoot(
displayTabsInGrid: Boolean = true,
mode: TabsTrayState.Mode = TabsTrayState.Mode.Normal,
normalTabs: List<TabSessionState> = emptyList(),
inactiveTabs: List<TabSessionState> = emptyList(),
privateTabs: List<TabSessionState> = emptyList(),
inactiveTabsExpanded: Boolean = false,
showInactiveTabsAutoCloseDialog: Boolean = false,
) {
val normalTabsState = remember { normalTabs.toMutableStateList() }
val inactiveTabsState = remember { inactiveTabs.toMutableStateList() }
val privateTabsState = remember { privateTabs.toMutableStateList() }
var inactiveTabsExpandedState by remember { mutableStateOf(inactiveTabsExpanded) }
var showInactiveTabsAutoCloseDialogState by remember { mutableStateOf(showInactiveTabsAutoCloseDialog) }
val appStore = AppStore(
initialState = AppState(
inactiveTabsExpanded = inactiveTabsExpandedState,
),
)
val tabsTrayStore = TabsTrayStore(
initialState = TabsTrayState(
mode = mode,
inactiveTabs = inactiveTabsState,
normalTabs = normalTabsState,
privateTabs = privateTabs,
),
)
FirefoxTheme {
TabsTray(
appStore = appStore,
tabsTrayStore = tabsTrayStore,
displayTabsInGrid = displayTabsInGrid,
shouldShowInactiveTabsAutoCloseDialog = { true },
onTabClose = { tab ->
if (tab.isNormalTab()) {
normalTabsState.remove(tab)
} else {
privateTabsState.remove(tab)
}
},
onTabMediaClick = {},
onTabClick = {},
onTabMultiSelectClick = {},
onTabLongClick = {},
onInactiveTabsHeaderClick = {
inactiveTabsExpandedState = !inactiveTabsExpandedState
},
onDeleteAllInactiveTabsClick = inactiveTabsState::clear,
onInactiveTabsAutoCloseDialogShown = {},
onInactiveTabAutoCloseDialogCloseButtonClick = {
showInactiveTabsAutoCloseDialogState = !showInactiveTabsAutoCloseDialogState
},
onEnableInactiveTabAutoCloseClick = {
showInactiveTabsAutoCloseDialogState = !showInactiveTabsAutoCloseDialogState
},
onInactiveTabClick = {},
onInactiveTabClose = inactiveTabsState::remove,
)
}
}
private fun generateFakeTabsList(tabCount: Int = 10, isPrivate: Boolean = false): List<TabSessionState> =
List(tabCount) { index ->
TabSessionState(
id = "tabId$index-$isPrivate",
content = ContentState(
url = "www.mozilla.com",
private = isPrivate,
),
)
}