From dfc5662e77e05bd9c641a9014a67e80c1799fdef Mon Sep 17 00:00:00 2001 From: Segun Famisa Date: Mon, 27 Apr 2026 14:21:36 +0200 Subject: [PATCH 1/3] Bug 2035740 - Extract AddFolderScreen into own file --- .../fenix/bookmarks/BookmarksScreen.kt | 125 +----------- .../fenix/bookmarks/ui/AddFolderScreen.kt | 183 ++++++++++++++++++ 2 files changed, 184 insertions(+), 124 deletions(-) create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/AddFolderScreen.kt diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt index d62be69d05ad3..729843653444a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt @@ -128,6 +128,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.bookmarks.BookmarksTestTag.BOOKMARK_TOOLBAR import org.mozilla.fenix.bookmarks.BookmarksTestTag.EDIT_BOOKMARK_ITEM_TITLE_TEXT_FIELD import org.mozilla.fenix.bookmarks.BookmarksTestTag.EDIT_BOOKMARK_ITEM_URL_TEXT_FIELD +import org.mozilla.fenix.bookmarks.ui.AddFolderScreen import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.components @@ -1776,88 +1777,6 @@ private fun EditFolderTopBar( ) } -@Composable -private fun AddFolderScreen( - store: BookmarksStore, -) { - val state by remember { store.stateFlow.map { it.bookmarksAddFolderState } } - .collectAsState(initial = store.state.bookmarksAddFolderState) - Scaffold( - topBar = { AddFolderTopBar(onBackClick = { store.dispatch(BackClicked) }) }, - ) { paddingValues -> - Box( - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth(), - contentAlignment = Alignment.TopCenter, - ) { - Column( - modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth), - ) { - TextField( - value = state?.folderBeingAddedTitle ?: "", - onValueChange = { newText -> store.dispatch(AddFolderAction.TitleChanged(newText)) }, - placeholder = "", - errorText = "", - modifier = Modifier - .padding( - start = 16.dp, - end = 16.dp, - top = 32.dp, - ) - .semantics { - testTagsAsResourceId = true - testTag = BookmarksTestTag.ADD_BOOKMARK_FOLDER_NAME_TEXT_FIELD - }, - label = stringResource(R.string.bookmark_name_label_normal_case), - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = stringResource(R.string.bookmark_save_in_label), - color = MaterialTheme.colorScheme.onSurface, - style = FirefoxTheme.typography.body2, - modifier = Modifier.padding(start = 16.dp), - ) - - IconListItem( - label = state?.parent?.title ?: "", - beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), - onClick = { store.dispatch(AddFolderAction.ParentFolderClicked) }, - ) - } - } - } -} - -@Composable -private fun AddFolderTopBar(onBackClick: () -> Unit) { - TopAppBar( - title = { - Text( - text = stringResource(R.string.bookmark_add_folder), - style = FirefoxTheme.typography.headline5, - ) - }, - navigationIcon = { - IconButton( - onClick = onBackClick, - contentDescription = stringResource(R.string.bookmark_navigate_back_button_content_description), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_back_24), - contentDescription = null, - ) - } - }, - windowInsets = WindowInsets( - top = 0.dp, - bottom = 0.dp, - ), - ) -} - @Composable private fun EditBookmarkScreen( store: BookmarksStore, @@ -2259,48 +2178,6 @@ private fun EmptyBookmarksScreenPreview() { private const val PREVIEW_BOOKMARKS_SIZE = 20 -@FlexibleWindowLightDarkPreview -@Composable -private fun AddFolderPreview() { - val store = BookmarksStore( - initialState = BookmarksState( - bookmarkItems = listOf(), - selectedItems = listOf(), - rootMenuShown = false, - showBookmarksImport = true, - sortMenuShown = false, - sortOrder = BookmarksListSortOrder.default, - recursiveSelectedCount = null, - currentFolder = BookmarkItem.Folder( - guid = BookmarkRoot.Mobile.id, - title = "Bookmarks", - position = null, - ), - isSignedIntoSync = false, - openTabsConfirmationDialog = OpenTabsConfirmationDialog.None, - bookmarksDeletionDialogState = DeletionDialogState.None, - bookmarksSnackbarState = BookmarksSnackbarState.None, - bookmarksEditBookmarkState = null, - bookmarksAddFolderState = BookmarksAddFolderState( - parent = BookmarkItem.Folder( - guid = BookmarkRoot.Mobile.id, - title = "Bookmarks", - position = null, - ), - folderBeingAddedTitle = "Edit me!", - ), - bookmarksSelectFolderState = null, - bookmarksEditFolderState = null, - bookmarksMultiselectMoveState = null, - isLoading = false, - isSearching = false, - ), - ) - FirefoxTheme { - AddFolderScreen(store) - } -} - private const val PREVIEW_INDENTATION_0 = 0 private const val PREVIEW_INDENTATION_1 = 1 private const val PREVIEW_INDENTATION_2 = 2 diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/AddFolderScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/AddFolderScreen.kt new file mode 100644 index 0000000000000..914a074f113db --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/AddFolderScreen.kt @@ -0,0 +1,183 @@ +/* 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.bookmarks.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.map +import mozilla.appservices.places.BookmarkRoot +import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview +import mozilla.components.compose.base.button.IconButton +import mozilla.components.compose.base.textfield.TextField +import org.mozilla.fenix.R +import org.mozilla.fenix.bookmarks.AddFolderAction +import org.mozilla.fenix.bookmarks.BackClicked +import org.mozilla.fenix.bookmarks.BookmarkItem +import org.mozilla.fenix.bookmarks.BookmarksAddFolderState +import org.mozilla.fenix.bookmarks.BookmarksListSortOrder +import org.mozilla.fenix.bookmarks.BookmarksSnackbarState +import org.mozilla.fenix.bookmarks.BookmarksState +import org.mozilla.fenix.bookmarks.BookmarksStore +import org.mozilla.fenix.bookmarks.BookmarksTestTag +import org.mozilla.fenix.bookmarks.DeletionDialogState +import org.mozilla.fenix.bookmarks.OpenTabsConfirmationDialog +import org.mozilla.fenix.compose.list.IconListItem +import org.mozilla.fenix.theme.FirefoxTheme +import mozilla.components.ui.icons.R as iconsR + +/** + * Top-level composable for the "Add folder" screen + */ +@Composable +internal fun AddFolderScreen( + modifier: Modifier = Modifier, + store: BookmarksStore, +) { + val state by remember { store.stateFlow.map { it.bookmarksAddFolderState } } + .collectAsState(initial = store.state.bookmarksAddFolderState) + + Scaffold( + modifier = modifier, + topBar = { AddFolderTopBar(onBackClick = { store.dispatch(BackClicked) }) }, + ) { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth(), + contentAlignment = Alignment.TopCenter, + ) { + Column( + modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth), + ) { + TextField( + value = state?.folderBeingAddedTitle ?: "", + onValueChange = { newText -> store.dispatch(AddFolderAction.TitleChanged(newText)) }, + placeholder = "", + errorText = "", + modifier = Modifier + .padding( + start = 16.dp, + end = 16.dp, + top = 32.dp, + ) + .semantics { + testTagsAsResourceId = true + testTag = BookmarksTestTag.ADD_BOOKMARK_FOLDER_NAME_TEXT_FIELD + }, + label = stringResource(R.string.bookmark_name_label_normal_case), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.bookmark_save_in_label), + color = MaterialTheme.colorScheme.onSurface, + style = FirefoxTheme.typography.body2, + modifier = Modifier.padding(start = 16.dp), + ) + + IconListItem( + label = state?.parent?.title ?: "", + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + onClick = { store.dispatch(AddFolderAction.ParentFolderClicked) }, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) // TopAppBar +@Composable +private fun AddFolderTopBar(onBackClick: () -> Unit) { + TopAppBar( + title = { + Text( + text = stringResource(R.string.bookmark_add_folder), + style = FirefoxTheme.typography.headline5, + ) + }, + navigationIcon = { + IconButton( + onClick = onBackClick, + contentDescription = stringResource(R.string.bookmark_navigate_back_button_content_description), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_back_24), + contentDescription = null, + ) + } + }, + windowInsets = WindowInsets( + top = 0.dp, + bottom = 0.dp, + ), + ) +} + +@FlexibleWindowLightDarkPreview +@Composable +private fun AddFolderPreview() { + val store = BookmarksStore( + initialState = BookmarksState( + bookmarkItems = listOf(), + selectedItems = listOf(), + rootMenuShown = false, + showBookmarksImport = true, + sortMenuShown = false, + sortOrder = BookmarksListSortOrder.default, + recursiveSelectedCount = null, + currentFolder = BookmarkItem.Folder( + guid = BookmarkRoot.Mobile.id, + title = "Bookmarks", + position = null, + ), + isSignedIntoSync = false, + openTabsConfirmationDialog = OpenTabsConfirmationDialog.None, + bookmarksDeletionDialogState = DeletionDialogState.None, + bookmarksSnackbarState = BookmarksSnackbarState.None, + bookmarksEditBookmarkState = null, + bookmarksAddFolderState = BookmarksAddFolderState( + parent = BookmarkItem.Folder( + guid = BookmarkRoot.Mobile.id, + title = "Bookmarks", + position = null, + ), + folderBeingAddedTitle = "Edit me!", + ), + bookmarksSelectFolderState = null, + bookmarksEditFolderState = null, + bookmarksMultiselectMoveState = null, + isLoading = false, + isSearching = false, + ), + ) + FirefoxTheme { + AddFolderScreen(modifier = Modifier, store = store) + } +} From 609e8eaa5ffa690b4226c168938d9ad50ca19453 Mon Sep 17 00:00:00 2001 From: Segun Famisa Date: Mon, 27 Apr 2026 14:34:53 +0200 Subject: [PATCH 2/3] Bug 2035740 - Extract EditFolderScreen into own file --- .../fenix/bookmarks/BookmarksScreen.kt | 167 +------------ .../fenix/bookmarks/ui/EditFolderScreen.kt | 219 ++++++++++++++++++ 2 files changed, 221 insertions(+), 165 deletions(-) create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/EditFolderScreen.kt diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt index 729843653444a..df5ef3cecaf9f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt @@ -129,6 +129,7 @@ import org.mozilla.fenix.bookmarks.BookmarksTestTag.BOOKMARK_TOOLBAR import org.mozilla.fenix.bookmarks.BookmarksTestTag.EDIT_BOOKMARK_ITEM_TITLE_TEXT_FIELD import org.mozilla.fenix.bookmarks.BookmarksTestTag.EDIT_BOOKMARK_ITEM_URL_TEXT_FIELD import org.mozilla.fenix.bookmarks.ui.AddFolderScreen +import org.mozilla.fenix.bookmarks.ui.EditFolderScreen import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.components @@ -945,7 +946,7 @@ private fun WarnDialog( } @Composable -private fun AlertDialogDeletionWarning( +internal fun AlertDialogDeletionWarning( onCancelTapped: () -> Unit, onDeleteTapped: () -> Unit, ) { @@ -1669,114 +1670,6 @@ private fun BookmarkListFolderMenu( ) } -@Composable -private fun EditFolderScreen( - store: BookmarksStore, -) { - val state by store.stateFlow.collectAsState() - val editState = state.bookmarksEditFolderState ?: return - val dialogState = state.bookmarksDeletionDialogState - - if (dialogState is DeletionDialogState.Presenting) { - AlertDialogDeletionWarning( - onCancelTapped = { store.dispatch(DeletionDialogAction.CancelTapped) }, - onDeleteTapped = { store.dispatch(DeletionDialogAction.DeleteTapped) }, - ) - } - - Scaffold( - topBar = { - EditFolderTopBar( - onBackClick = { store.dispatch(BackClicked) }, - onDeleteClick = { store.dispatch(EditFolderAction.DeleteClicked) }, - ) - }, - ) { paddingValues -> - Box( - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth(), - contentAlignment = Alignment.TopCenter, - ) { - Column( - modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth), - ) { - TextField( - value = editState.folder.title, - onValueChange = { newText -> - store.dispatch(EditFolderAction.TitleChanged(newText)) - }, - placeholder = "", - errorText = "", - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - top = 32.dp, - ), - label = stringResource(R.string.bookmark_name_label_normal_case), - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - stringResource(R.string.bookmark_save_in_label), - color = MaterialTheme.colorScheme.onSurface, - style = FirefoxTheme.typography.body2, - modifier = Modifier.padding(start = 16.dp), - ) - - IconListItem( - label = editState.parent.title, - beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), - onClick = { store.dispatch(EditFolderAction.ParentFolderClicked) }, - ) - } - } - } -} - -@Composable -private fun EditFolderTopBar( - onBackClick: () -> Unit, - onDeleteClick: () -> Unit, -) { - TopAppBar( - title = { - Text( - text = stringResource(R.string.edit_bookmark_folder_fragment_title), - style = FirefoxTheme.typography.headline5, - ) - }, - navigationIcon = { - IconButton( - onClick = onBackClick, - contentDescription = stringResource(R.string.bookmark_navigate_back_button_content_description), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_back_24), - contentDescription = null, - ) - } - }, - actions = { - IconButton( - onClick = onDeleteClick, - contentDescription = stringResource(R.string.bookmark_delete_folder_content_description), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_delete_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - } - }, - windowInsets = WindowInsets( - top = 0.dp, - bottom = 0.dp, - ), - ) -} - @Composable private fun EditBookmarkScreen( store: BookmarksStore, @@ -2014,62 +1907,6 @@ private fun EditBookmarkScreenPreview() { } } -@Composable -@FlexibleWindowLightDarkPreview -private fun EditFolderScreenPreview() { - val store = BookmarksStore( - initialState = BookmarksState( - bookmarkItems = listOf(), - selectedItems = listOf(), - rootMenuShown = false, - showBookmarksImport = true, - sortMenuShown = false, - sortOrder = BookmarksListSortOrder.default, - recursiveSelectedCount = null, - currentFolder = BookmarkItem.Folder( - guid = BookmarkRoot.Mobile.id, - title = "Bookmarks", - position = null, - ), - isSignedIntoSync = true, - openTabsConfirmationDialog = OpenTabsConfirmationDialog.None, - bookmarksDeletionDialogState = DeletionDialogState.None, - bookmarksSnackbarState = BookmarksSnackbarState.None, - bookmarksAddFolderState = null, - bookmarksEditBookmarkState = BookmarksEditBookmarkState( - bookmark = BookmarkItem.Bookmark( - url = "https://www.whoevenmakeswebaddressesthislonglikeseriously1.com", - title = "this is a very long bookmark title that should overflow 1", - previewImageUrl = "", - guid = "1", - position = null, - ), - folder = BookmarkItem.Folder("folder 1", guid = "1", position = null), - ), - bookmarksSelectFolderState = null, - bookmarksEditFolderState = BookmarksEditFolderState( - parent = BookmarkItem.Folder( - guid = BookmarkRoot.Mobile.id, - title = "Bookmarks", - position = null, - ), - folder = BookmarkItem.Folder( - guid = BookmarkRoot.Mobile.id, - title = "New folder", - position = null, - ), - ), - bookmarksMultiselectMoveState = null, - isLoading = false, - isSearching = false, - ), - ) - - FirefoxTheme { - EditFolderScreen(store = store) - } -} - @Composable @FlexibleWindowLightDarkPreview private fun BookmarksScreenPreview() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/EditFolderScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/EditFolderScreen.kt new file mode 100644 index 0000000000000..ad832548382e7 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/EditFolderScreen.kt @@ -0,0 +1,219 @@ +/* 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.bookmarks.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import mozilla.appservices.places.BookmarkRoot +import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview +import mozilla.components.compose.base.button.IconButton +import mozilla.components.compose.base.textfield.TextField +import org.mozilla.fenix.R +import org.mozilla.fenix.bookmarks.AlertDialogDeletionWarning +import org.mozilla.fenix.bookmarks.BackClicked +import org.mozilla.fenix.bookmarks.BookmarkItem +import org.mozilla.fenix.bookmarks.BookmarksEditBookmarkState +import org.mozilla.fenix.bookmarks.BookmarksEditFolderState +import org.mozilla.fenix.bookmarks.BookmarksListSortOrder +import org.mozilla.fenix.bookmarks.BookmarksSnackbarState +import org.mozilla.fenix.bookmarks.BookmarksState +import org.mozilla.fenix.bookmarks.BookmarksStore +import org.mozilla.fenix.bookmarks.DeletionDialogAction +import org.mozilla.fenix.bookmarks.DeletionDialogState +import org.mozilla.fenix.bookmarks.EditFolderAction +import org.mozilla.fenix.bookmarks.OpenTabsConfirmationDialog +import org.mozilla.fenix.compose.list.IconListItem +import org.mozilla.fenix.theme.FirefoxTheme +import mozilla.components.ui.icons.R as iconsR + +/** + * Top-level composable for the "Edit folder" screen + */ +@Composable +internal fun EditFolderScreen( + modifier: Modifier = Modifier, + store: BookmarksStore, +) { + val state by store.stateFlow.collectAsState() + val editState = state.bookmarksEditFolderState ?: return + val dialogState = state.bookmarksDeletionDialogState + + if (dialogState is DeletionDialogState.Presenting) { + AlertDialogDeletionWarning( + onCancelTapped = { store.dispatch(DeletionDialogAction.CancelTapped) }, + onDeleteTapped = { store.dispatch(DeletionDialogAction.DeleteTapped) }, + ) + } + + Scaffold( + modifier = modifier, + topBar = { + EditFolderTopBar( + onBackClick = { store.dispatch(BackClicked) }, + onDeleteClick = { store.dispatch(EditFolderAction.DeleteClicked) }, + ) + }, + ) { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth(), + contentAlignment = Alignment.TopCenter, + ) { + Column( + modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth), + ) { + TextField( + value = editState.folder.title, + onValueChange = { newText -> + store.dispatch(EditFolderAction.TitleChanged(newText)) + }, + placeholder = "", + errorText = "", + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + top = 32.dp, + ), + label = stringResource(R.string.bookmark_name_label_normal_case), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + stringResource(R.string.bookmark_save_in_label), + color = MaterialTheme.colorScheme.onSurface, + style = FirefoxTheme.typography.body2, + modifier = Modifier.padding(start = 16.dp), + ) + + IconListItem( + label = editState.parent.title, + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + onClick = { store.dispatch(EditFolderAction.ParentFolderClicked) }, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) // TopAppBar +@Composable +private fun EditFolderTopBar( + onBackClick: () -> Unit, + onDeleteClick: () -> Unit, +) { + TopAppBar( + title = { + Text( + text = stringResource(R.string.edit_bookmark_folder_fragment_title), + style = FirefoxTheme.typography.headline5, + ) + }, + navigationIcon = { + IconButton( + onClick = onBackClick, + contentDescription = stringResource(R.string.bookmark_navigate_back_button_content_description), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_back_24), + contentDescription = null, + ) + } + }, + actions = { + IconButton( + onClick = onDeleteClick, + contentDescription = stringResource(R.string.bookmark_delete_folder_content_description), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_delete_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + windowInsets = WindowInsets( + top = 0.dp, + bottom = 0.dp, + ), + ) +} + +@Composable +@FlexibleWindowLightDarkPreview +private fun EditFolderScreenPreview() { + val store = BookmarksStore( + initialState = BookmarksState( + bookmarkItems = listOf(), + selectedItems = listOf(), + rootMenuShown = false, + showBookmarksImport = true, + sortMenuShown = false, + sortOrder = BookmarksListSortOrder.default, + recursiveSelectedCount = null, + currentFolder = BookmarkItem.Folder( + guid = BookmarkRoot.Mobile.id, + title = "Bookmarks", + position = null, + ), + isSignedIntoSync = true, + openTabsConfirmationDialog = OpenTabsConfirmationDialog.None, + bookmarksDeletionDialogState = DeletionDialogState.None, + bookmarksSnackbarState = BookmarksSnackbarState.None, + bookmarksAddFolderState = null, + bookmarksEditBookmarkState = BookmarksEditBookmarkState( + bookmark = BookmarkItem.Bookmark( + url = "https://www.whoevenmakeswebaddressesthislonglikeseriously1.com", + title = "this is a very long bookmark title that should overflow 1", + previewImageUrl = "", + guid = "1", + position = null, + ), + folder = BookmarkItem.Folder("folder 1", guid = "1", position = null), + ), + bookmarksSelectFolderState = null, + bookmarksEditFolderState = BookmarksEditFolderState( + parent = BookmarkItem.Folder( + guid = BookmarkRoot.Mobile.id, + title = "Bookmarks", + position = null, + ), + folder = BookmarkItem.Folder( + guid = BookmarkRoot.Mobile.id, + title = "New folder", + position = null, + ), + ), + bookmarksMultiselectMoveState = null, + isLoading = false, + isSearching = false, + ), + ) + + FirefoxTheme { + EditFolderScreen(store = store) + } +} From ade3d13eb066a96f370b97033165d50b8321b30e Mon Sep 17 00:00:00 2001 From: Segun Famisa Date: Tue, 28 Apr 2026 12:13:51 +0200 Subject: [PATCH 3/3] Bug 2035740 - Extract SelectFolderScreen into own file --- .../fenix/bookmarks/BookmarksScreen.kt | 489 +-------------- .../fenix/bookmarks/ui/SelectFolderScreen.kt | 563 ++++++++++++++++++ 2 files changed, 564 insertions(+), 488 deletions(-) create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/SelectFolderScreen.kt diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt index df5ef3cecaf9f..3b07269bd7702 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt @@ -30,19 +30,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -77,15 +72,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CollectionInfo import androidx.compose.ui.semantics.CollectionItemInfo -import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.collectionInfo import androidx.compose.ui.semantics.collectionItemInfo import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -123,18 +115,17 @@ import mozilla.components.compose.browser.toolbar.ui.BrowserToolbarQuery import mozilla.components.concept.base.profiler.Profiler import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.support.ktx.android.view.hideKeyboard -import org.mozilla.fenix.Config import org.mozilla.fenix.R import org.mozilla.fenix.bookmarks.BookmarksTestTag.BOOKMARK_TOOLBAR import org.mozilla.fenix.bookmarks.BookmarksTestTag.EDIT_BOOKMARK_ITEM_TITLE_TEXT_FIELD import org.mozilla.fenix.bookmarks.BookmarksTestTag.EDIT_BOOKMARK_ITEM_URL_TEXT_FIELD import org.mozilla.fenix.bookmarks.ui.AddFolderScreen import org.mozilla.fenix.bookmarks.ui.EditFolderScreen +import org.mozilla.fenix.bookmarks.ui.SelectFolderScreen import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.components import org.mozilla.fenix.compose.Favicon -import org.mozilla.fenix.compose.list.IconListItem import org.mozilla.fenix.compose.list.SelectableFaviconListItem import org.mozilla.fenix.compose.list.SelectableIconListItem import org.mozilla.fenix.ext.components @@ -973,360 +964,6 @@ internal fun AlertDialogDeletionWarning( ) } -@Composable -private fun SelectFolderScreen( - store: BookmarksStore, -) { - val showNewFolderButton by remember { store.stateFlow.map { store.state.showNewFolderButton } } - .collectAsState(initial = store.state.showNewFolderButton) - val state by remember { store.stateFlow.map { it.bookmarksSelectFolderState } } - .collectAsState(initial = store.state.bookmarksSelectFolderState) - - LaunchedEffect(Unit) { - store.dispatch(SelectFolderAction.ViewAppeared) - } - - BackInvokedHandler(state?.isSearching ?: false) { - store.dispatch(SelectFolderAction.SearchDismissed) - } - - val focusManager = LocalFocusManager.current - val keyboardController = LocalSoftwareKeyboardController.current - - Scaffold( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures( - onTap = { - focusManager.clearFocus() - keyboardController?.hide() - store.dispatch(SelectFolderAction.SearchDismissed) - }, - ) - }, - topBar = { - if (state?.isSearching ?: false) { - SelectFolderSearchTopBar(store = store) - } else { - SelectFolderTopBar(store = store) - } - }, - ) { paddingValues -> - if (state?.isLoading ?: false) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - return@Scaffold - } - LazyColumn( - modifier = Modifier - .padding(paddingValues) - .padding(vertical = 16.dp) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - items( - key = { item -> item.folder.guid }, - items = state?.visibleFolders.orEmpty().flattenToList(), - ) { folder -> - FolderListItem( - folder = folder, - isSelected = folder.guid == state?.selectedGuid, - showPadding = state?.isSearching ?: true, - onClick = { store.dispatch(SelectFolderAction.ItemClicked(folder)) }, - onChevronClick = { store.dispatch(SelectFolderAction.ChevronClicked(folder)) }, - ) - } - - if (showNewFolderButton) { - item { - NewFolderListItem { store.dispatch(AddFolderClicked) } - } - } - } - } -} - -@Composable -private fun SelectFolderSearchTopBar(store: BookmarksStore) { - val focusRequester = remember { FocusRequester() } - var text by remember { - mutableStateOf( - TextFieldValue( - store.state.bookmarksSelectFolderState?.searchQuery.orEmpty(), - selection = TextRange( - store.state.bookmarksSelectFolderState?.searchQuery?.length ?: 0, - ), - ), - ) - } - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - val value = text.text - text = text.copy(selection = TextRange(value.length)) - } - - TopAppBar( - title = { - OutlinedTextField( - value = text, - onValueChange = { newValue -> - text = newValue - store.dispatch( - SelectFolderAction.SearchQueryUpdated(newValue.text), - ) - }, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - placeholder = { - stringResource(R.string.select_bookmark_search_button_content_description) - }, - leadingIcon = { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_search_24), - contentDescription = stringResource( - R.string.select_bookmark_search_button_content_description, - ), - ) - }, - singleLine = true, - shape = RoundedCornerShape(12.dp), - ) - }, - navigationIcon = {}, - actions = {}, - windowInsets = WindowInsets( - top = 0.dp, - bottom = 0.dp, - ), - ) -} - -@Composable -private fun FolderListItem( - folder: SelectFolderItem, - isSelected: Boolean, - showPadding: Boolean = true, - onClick: () -> Unit, - onChevronClick: () -> Unit, -) { - if (folder.isDesktopRoot) { - Box( - modifier = Modifier.padding( - start = folder.startPadding, - ), - ) { - Row(modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth)) { - Spacer(modifier = Modifier.width(56.dp)) - Text( - text = folder.title, - color = MaterialTheme.colorScheme.tertiary, - style = FirefoxTheme.typography.headline8, - ) - } - } - } else { - Box( - modifier = Modifier - .padding(start = if (!showPadding) folder.startPadding else 0.dp) - .width(FirefoxTheme.layout.size.containerMaxWidth), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - when (folder.expansionState) { - is SelectFolderExpansionState.None -> Spacer(modifier = Modifier.size(width = 48.dp, height = 0.dp)) - is SelectFolderExpansionState.Open -> { - IconButton( - onClick = onChevronClick, - contentDescription = stringResource( - R.string.bookmark_select_folder_close_folder_content_description, - folder.title, - ), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_chevron_down_24), - contentDescription = null, - ) - } - } - is SelectFolderExpansionState.Closed -> { - IconButton( - onClick = onChevronClick, - contentDescription = stringResource( - R.string.bookmark_select_folder_expand_folder_content_description, - folder.title, - ), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), - contentDescription = null, - ) - } - } - } - SelectableIconListItem( - label = folder.title, - isSelected = isSelected, - beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), - modifier = Modifier - .toggleable( - value = isSelected, - role = Role.RadioButton, - onValueChange = { onClick() }, - ), - ) - } - } - } -} - -@Composable -private fun NewFolderListItem(onClick: () -> Unit) { - IconListItem( - label = stringResource(R.string.bookmark_select_folder_new_folder_button_title), - modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth), - colors = ListItemDefaults.colors( - headlineColor = MaterialTheme.colorScheme.tertiary, - ), - beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_add_24), - beforeIconTint = MaterialTheme.colorScheme.tertiary, - onClick = onClick, - ) -} - -@Composable -private fun SelectFolderTopBar(store: BookmarksStore) { - val onNewFolderClick = store.state.showNewFolderButton.takeIf { it }?.let { - { store.dispatch(AddFolderClicked) } - } - TopAppBar( - title = { - Text( - text = stringResource(R.string.bookmark_select_folder_fragment_label), - style = FirefoxTheme.typography.headline5, - ) - }, - navigationIcon = { - IconButton( - onClick = { store.dispatch(BackClicked) }, - contentDescription = stringResource(R.string.bookmark_navigate_back_button_content_description), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_back_24), - contentDescription = null, - ) - } - }, - actions = { - SelectFolderTopBarActions( - store = store, - onNewFolderClick = onNewFolderClick, - ) - }, - windowInsets = WindowInsets( - top = 0.dp, - bottom = 0.dp, - ), - ) -} - -@Composable -private fun SelectFolderTopBarActions( - store: BookmarksStore, - onNewFolderClick: (() -> Unit)?, -) { - Box { - IconButton( - onClick = { - store.dispatch(BookmarksListMenuAction.SortMenu.SortMenuButtonClicked) - }, - contentDescription = stringResource( - R.string.bookmark_sort_menu_content_desc, - ), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_sort_24), - contentDescription = null, - ) - } - - SelectFolderSortOverflowMenu(store = store) - } - - // TODO https://bugzilla.mozilla.org/show_bug.cgi?id=2006505 - if (Config.channel.isDebug) { - IconButton( - onClick = { - store.dispatch(SelectFolderAction.SearchClicked) - }, - contentDescription = stringResource( - R.string.select_bookmark_search_button_content_description, - ), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_search_24), - contentDescription = null, - ) - } - } - - if (onNewFolderClick != null) { - IconButton( - onClick = { onNewFolderClick() }, - contentDescription = stringResource( - R.string.bookmark_add_new_folder_button_content_description, - ), - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_folder_add_24), - contentDescription = null, - ) - } - } -} - -@Composable -private fun SelectFolderSortOverflowMenu(store: BookmarksStore) { - val showMenu by remember { store.stateFlow.map { store.state.sortMenuShown } } - .collectAsState(initial = store.state.sortMenuShown) - val sortOrder by remember { store.stateFlow.map { store.state.sortOrder } } - .collectAsState(initial = store.state.sortOrder) - - val menuItems = listOf( - MenuItem.CheckableItem( - text = Text.Resource(R.string.bookmark_sort_menu_custom), - isChecked = sortOrder is BookmarksListSortOrder.Positional, - onClick = { store.dispatch(SelectFolderAction.SortMenu.CustomSortClicked) }, - ), - MenuItem.CheckableItem( - text = Text.Resource(R.string.bookmark_sort_menu_newest), - isChecked = sortOrder == BookmarksListSortOrder.Created(ascending = true), - onClick = { store.dispatch(SelectFolderAction.SortMenu.NewestClicked) }, - ), - MenuItem.CheckableItem( - text = Text.Resource(R.string.bookmark_sort_menu_oldest), - isChecked = sortOrder == BookmarksListSortOrder.Created(ascending = false), - onClick = { store.dispatch(SelectFolderAction.SortMenu.OldestClicked) }, - ), - MenuItem.CheckableItem( - text = Text.Resource(R.string.bookmark_sort_menu_a_to_z), - isChecked = sortOrder == BookmarksListSortOrder.Alphabetical(ascending = true), - onClick = { store.dispatch(SelectFolderAction.SortMenu.AtoZClicked) }, - ), - MenuItem.CheckableItem( - text = Text.Resource(R.string.bookmark_sort_menu_z_to_a), - isChecked = sortOrder == BookmarksListSortOrder.Alphabetical(ascending = false), - onClick = { store.dispatch(SelectFolderAction.SortMenu.ZtoAClicked) }, - ), - ) - DropdownMenu( - menuItems = menuItems, - expanded = showMenu, - onDismissRequest = { store.dispatch(SelectFolderAction.SortMenu.SortMenuDismissed) }, - ) -} - @Composable private fun RootBookmarksOverflowMenu(store: BookmarksStore) { val showMenu by remember { store.stateFlow.map { store.state.rootMenuShown } } @@ -2014,127 +1651,3 @@ private fun EmptyBookmarksScreenPreview() { } private const val PREVIEW_BOOKMARKS_SIZE = 20 - -private const val PREVIEW_INDENTATION_0 = 0 -private const val PREVIEW_INDENTATION_1 = 1 -private const val PREVIEW_INDENTATION_2 = 2 -private const val PREVIEW_INDENTATION_3 = 3 - -@FlexibleWindowLightDarkPreview -@Suppress("detekt.LongMethod") -@Composable -private fun SelectFolderPreview() { - val store = BookmarksStore( - initialState = BookmarksState( - bookmarkItems = listOf(), - selectedItems = listOf(), - rootMenuShown = false, - showBookmarksImport = true, - sortMenuShown = false, - sortOrder = BookmarksListSortOrder.default, - recursiveSelectedCount = null, - currentFolder = BookmarkItem.Folder( - guid = BookmarkRoot.Mobile.id, - title = "Bookmarks", - position = null, - ), - isSignedIntoSync = false, - bookmarksEditBookmarkState = null, - bookmarksAddFolderState = BookmarksAddFolderState( - parent = BookmarkItem.Folder( - guid = BookmarkRoot.Mobile.id, - title = "Bookmarks", - position = null, - ), - folderBeingAddedTitle = "Edit me!", - ), - openTabsConfirmationDialog = OpenTabsConfirmationDialog.None, - bookmarksDeletionDialogState = DeletionDialogState.None, - bookmarksSnackbarState = BookmarksSnackbarState.None, - bookmarksEditFolderState = null, - bookmarksSelectFolderState = BookmarksSelectFolderState( - outerSelectionGuid = "", - innerSelectionGuid = "guid1", - folders = listOf( - SelectFolderItem( - indentation = PREVIEW_INDENTATION_0, - folder = BookmarkItem.Folder("Bookmarks", "guid0", null), - expansionState = SelectFolderExpansionState.Closed, - ), - SelectFolderItem( - indentation = PREVIEW_INDENTATION_0, - folder = BookmarkItem.Folder("Bookmarks Menu", BookmarkRoot.Menu.id, null), - expansionState = SelectFolderExpansionState.None, - ), - SelectFolderItem( - indentation = PREVIEW_INDENTATION_0, - folder = BookmarkItem.Folder("Bookmarks Toolbar", BookmarkRoot.Toolbar.id, position = null), - expansionState = SelectFolderExpansionState.None, - ), - SelectFolderItem( - indentation = PREVIEW_INDENTATION_1, - folder = BookmarkItem.Folder("Desktop Bookmarks", BookmarkRoot.Root.id, position = null), - expansionState = SelectFolderExpansionState.None, - ), - SelectFolderItem( - indentation = PREVIEW_INDENTATION_0, - folder = BookmarkItem.Folder("Bookmarks Unfiled", BookmarkRoot.Unfiled.id, position = null), - expansionState = SelectFolderExpansionState.Open( - listOf( - SelectFolderItem( - indentation = PREVIEW_INDENTATION_1, - folder = BookmarkItem.Folder("Nested One", "guid0", position = null), - expansionState = SelectFolderExpansionState.Open( - listOf( - SelectFolderItem( - indentation = PREVIEW_INDENTATION_2, - folder = BookmarkItem.Folder("Nested Two", "guid0", position = null), - expansionState = SelectFolderExpansionState.None, - ), - SelectFolderItem( - indentation = PREVIEW_INDENTATION_2, - folder = BookmarkItem.Folder("Nested Two", "guid0", position = null), - expansionState = SelectFolderExpansionState.None, - ), - ), - ), - ), - SelectFolderItem( - indentation = PREVIEW_INDENTATION_1, - folder = BookmarkItem.Folder("Nested One", "guid0", position = null), - expansionState = SelectFolderExpansionState.Open( - listOf( - SelectFolderItem( - indentation = PREVIEW_INDENTATION_2, - folder = BookmarkItem.Folder("Nested Two", "guid1", position = null), - expansionState = SelectFolderExpansionState.Open( - listOf( - SelectFolderItem( - indentation = PREVIEW_INDENTATION_3, - folder = BookmarkItem.Folder( - title = "Nested Three", - guid = "guid0", - position = null, - ), - expansionState = SelectFolderExpansionState.None, - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - bookmarksMultiselectMoveState = null, - isLoading = false, - isSearching = false, - ), - ) - FirefoxTheme { - SelectFolderScreen(store) - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/SelectFolderScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/SelectFolderScreen.kt new file mode 100644 index 0000000000000..898c9e3ff20c9 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/ui/SelectFolderScreen.kt @@ -0,0 +1,563 @@ +/* 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.bookmarks.ui + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.map +import mozilla.appservices.places.BookmarkRoot +import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview +import mozilla.components.compose.base.button.IconButton +import mozilla.components.compose.base.menu.DropdownMenu +import mozilla.components.compose.base.menu.MenuItem +import mozilla.components.compose.base.text.Text +import mozilla.components.compose.base.utils.BackInvokedHandler +import org.mozilla.fenix.Config +import org.mozilla.fenix.R +import org.mozilla.fenix.bookmarks.AddFolderClicked +import org.mozilla.fenix.bookmarks.BackClicked +import org.mozilla.fenix.bookmarks.BookmarkItem +import org.mozilla.fenix.bookmarks.BookmarksAddFolderState +import org.mozilla.fenix.bookmarks.BookmarksListMenuAction +import org.mozilla.fenix.bookmarks.BookmarksListSortOrder +import org.mozilla.fenix.bookmarks.BookmarksSelectFolderState +import org.mozilla.fenix.bookmarks.BookmarksSnackbarState +import org.mozilla.fenix.bookmarks.BookmarksState +import org.mozilla.fenix.bookmarks.BookmarksStore +import org.mozilla.fenix.bookmarks.DeletionDialogState +import org.mozilla.fenix.bookmarks.OpenTabsConfirmationDialog +import org.mozilla.fenix.bookmarks.SelectFolderAction +import org.mozilla.fenix.bookmarks.SelectFolderExpansionState +import org.mozilla.fenix.bookmarks.SelectFolderItem +import org.mozilla.fenix.bookmarks.flattenToList +import org.mozilla.fenix.compose.list.IconListItem +import org.mozilla.fenix.compose.list.SelectableIconListItem +import org.mozilla.fenix.theme.FirefoxTheme +import mozilla.components.ui.icons.R as iconsR + +/** + * Top-level composable for the "Select folder" screen + */ +@Composable +internal fun SelectFolderScreen( + modifier: Modifier = Modifier, + store: BookmarksStore, +) { + val showNewFolderButton by remember { store.stateFlow.map { store.state.showNewFolderButton } } + .collectAsState(initial = store.state.showNewFolderButton) + val state by remember { store.stateFlow.map { it.bookmarksSelectFolderState } } + .collectAsState(initial = store.state.bookmarksSelectFolderState) + + LaunchedEffect(Unit) { + store.dispatch(SelectFolderAction.ViewAppeared) + } + + BackInvokedHandler(state?.isSearching ?: false) { + store.dispatch(SelectFolderAction.SearchDismissed) + } + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + Scaffold( + modifier = modifier + .pointerInput(Unit) { + detectTapGestures( + onTap = { + focusManager.clearFocus() + keyboardController?.hide() + store.dispatch(SelectFolderAction.SearchDismissed) + }, + ) + }, + topBar = { + if (state?.isSearching ?: false) { + SelectFolderSearchTopBar(store = store) + } else { + SelectFolderTopBar(store = store) + } + }, + ) { paddingValues -> + if (state?.isLoading ?: false) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + return@Scaffold + } + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .padding(vertical = 16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items( + key = { item -> item.folder.guid }, + items = state?.visibleFolders.orEmpty().flattenToList(), + ) { folder -> + FolderListItem( + folder = folder, + isSelected = folder.guid == state?.selectedGuid, + showPadding = state?.isSearching ?: true, + onClick = { store.dispatch(SelectFolderAction.ItemClicked(folder)) }, + onChevronClick = { store.dispatch(SelectFolderAction.ChevronClicked(folder)) }, + ) + } + + if (showNewFolderButton) { + item { + NewFolderListItem { store.dispatch(AddFolderClicked) } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) // TopAppBar +@Composable +private fun SelectFolderSearchTopBar(store: BookmarksStore) { + val focusRequester = remember { FocusRequester() } + var text by remember { + mutableStateOf( + TextFieldValue( + store.state.bookmarksSelectFolderState?.searchQuery.orEmpty(), + selection = TextRange( + store.state.bookmarksSelectFolderState?.searchQuery?.length ?: 0, + ), + ), + ) + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + val value = text.text + text = text.copy(selection = TextRange(value.length)) + } + + TopAppBar( + title = { + OutlinedTextField( + value = text, + onValueChange = { newValue -> + text = newValue + store.dispatch( + SelectFolderAction.SearchQueryUpdated(newValue.text), + ) + }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + placeholder = { + stringResource(R.string.select_bookmark_search_button_content_description) + }, + leadingIcon = { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_search_24), + contentDescription = stringResource( + R.string.select_bookmark_search_button_content_description, + ), + ) + }, + singleLine = true, + shape = RoundedCornerShape(12.dp), + ) + }, + navigationIcon = {}, + actions = {}, + windowInsets = WindowInsets( + top = 0.dp, + bottom = 0.dp, + ), + ) +} + +@Composable +private fun FolderListItem( + folder: SelectFolderItem, + isSelected: Boolean, + showPadding: Boolean = true, + onClick: () -> Unit, + onChevronClick: () -> Unit, +) { + if (folder.isDesktopRoot) { + Box( + modifier = Modifier.padding( + start = folder.startPadding, + ), + ) { + Row(modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth)) { + Spacer(modifier = Modifier.width(56.dp)) + Text( + text = folder.title, + color = MaterialTheme.colorScheme.tertiary, + style = FirefoxTheme.typography.headline8, + ) + } + } + } else { + Box( + modifier = Modifier + .padding(start = if (!showPadding) folder.startPadding else 0.dp) + .width(FirefoxTheme.layout.size.containerMaxWidth), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + when (folder.expansionState) { + is SelectFolderExpansionState.None -> Spacer(modifier = Modifier.size(width = 48.dp, height = 0.dp)) + is SelectFolderExpansionState.Open -> { + IconButton( + onClick = onChevronClick, + contentDescription = stringResource( + R.string.bookmark_select_folder_close_folder_content_description, + folder.title, + ), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_chevron_down_24), + contentDescription = null, + ) + } + } + is SelectFolderExpansionState.Closed -> { + IconButton( + onClick = onChevronClick, + contentDescription = stringResource( + R.string.bookmark_select_folder_expand_folder_content_description, + folder.title, + ), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), + contentDescription = null, + ) + } + } + } + SelectableIconListItem( + label = folder.title, + isSelected = isSelected, + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + modifier = Modifier + .toggleable( + value = isSelected, + role = Role.RadioButton, + onValueChange = { onClick() }, + ), + ) + } + } + } +} + +@Composable +private fun NewFolderListItem(onClick: () -> Unit) { + IconListItem( + label = stringResource(R.string.bookmark_select_folder_new_folder_button_title), + modifier = Modifier.width(FirefoxTheme.layout.size.containerMaxWidth), + colors = ListItemDefaults.colors( + headlineColor = MaterialTheme.colorScheme.tertiary, + ), + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_add_24), + beforeIconTint = MaterialTheme.colorScheme.tertiary, + onClick = onClick, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) // TopAppBar +@Composable +private fun SelectFolderTopBar(store: BookmarksStore) { + val onNewFolderClick = store.state.showNewFolderButton.takeIf { it }?.let { + { store.dispatch(AddFolderClicked) } + } + TopAppBar( + title = { + Text( + text = stringResource(R.string.bookmark_select_folder_fragment_label), + style = FirefoxTheme.typography.headline5, + ) + }, + navigationIcon = { + IconButton( + onClick = { store.dispatch(BackClicked) }, + contentDescription = stringResource(R.string.bookmark_navigate_back_button_content_description), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_back_24), + contentDescription = null, + ) + } + }, + actions = { + SelectFolderTopBarActions( + store = store, + onNewFolderClick = onNewFolderClick, + ) + }, + windowInsets = WindowInsets( + top = 0.dp, + bottom = 0.dp, + ), + ) +} + +@Composable +private fun SelectFolderTopBarActions( + store: BookmarksStore, + onNewFolderClick: (() -> Unit)?, +) { + Box { + IconButton( + onClick = { + store.dispatch(BookmarksListMenuAction.SortMenu.SortMenuButtonClicked) + }, + contentDescription = stringResource( + R.string.bookmark_sort_menu_content_desc, + ), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_sort_24), + contentDescription = null, + ) + } + + SelectFolderSortOverflowMenu(store = store) + } + + // TODO https://bugzilla.mozilla.org/show_bug.cgi?id=2006505 + if (Config.channel.isDebug) { + IconButton( + onClick = { + store.dispatch(SelectFolderAction.SearchClicked) + }, + contentDescription = stringResource( + R.string.select_bookmark_search_button_content_description, + ), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_search_24), + contentDescription = null, + ) + } + } + + if (onNewFolderClick != null) { + IconButton( + onClick = { onNewFolderClick() }, + contentDescription = stringResource( + R.string.bookmark_add_new_folder_button_content_description, + ), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_folder_add_24), + contentDescription = null, + ) + } + } +} + +@Composable +private fun SelectFolderSortOverflowMenu(store: BookmarksStore) { + val showMenu by remember { store.stateFlow.map { store.state.sortMenuShown } } + .collectAsState(initial = store.state.sortMenuShown) + val sortOrder by remember { store.stateFlow.map { store.state.sortOrder } } + .collectAsState(initial = store.state.sortOrder) + + val menuItems = listOf( + MenuItem.CheckableItem( + text = Text.Resource(R.string.bookmark_sort_menu_custom), + isChecked = sortOrder is BookmarksListSortOrder.Positional, + onClick = { store.dispatch(SelectFolderAction.SortMenu.CustomSortClicked) }, + ), + MenuItem.CheckableItem( + text = Text.Resource(R.string.bookmark_sort_menu_newest), + isChecked = sortOrder == BookmarksListSortOrder.Created(ascending = true), + onClick = { store.dispatch(SelectFolderAction.SortMenu.NewestClicked) }, + ), + MenuItem.CheckableItem( + text = Text.Resource(R.string.bookmark_sort_menu_oldest), + isChecked = sortOrder == BookmarksListSortOrder.Created(ascending = false), + onClick = { store.dispatch(SelectFolderAction.SortMenu.OldestClicked) }, + ), + MenuItem.CheckableItem( + text = Text.Resource(R.string.bookmark_sort_menu_a_to_z), + isChecked = sortOrder == BookmarksListSortOrder.Alphabetical(ascending = true), + onClick = { store.dispatch(SelectFolderAction.SortMenu.AtoZClicked) }, + ), + MenuItem.CheckableItem( + text = Text.Resource(R.string.bookmark_sort_menu_z_to_a), + isChecked = sortOrder == BookmarksListSortOrder.Alphabetical(ascending = false), + onClick = { store.dispatch(SelectFolderAction.SortMenu.ZtoAClicked) }, + ), + ) + DropdownMenu( + menuItems = menuItems, + expanded = showMenu, + onDismissRequest = { store.dispatch(SelectFolderAction.SortMenu.SortMenuDismissed) }, + ) +} + +private const val PREVIEW_INDENTATION_0 = 0 +private const val PREVIEW_INDENTATION_1 = 1 +private const val PREVIEW_INDENTATION_2 = 2 +private const val PREVIEW_INDENTATION_3 = 3 + +@FlexibleWindowLightDarkPreview +@Suppress("detekt.LongMethod") +@Composable +private fun SelectFolderPreview() { + val store = BookmarksStore( + initialState = BookmarksState( + bookmarkItems = listOf(), + selectedItems = listOf(), + rootMenuShown = false, + showBookmarksImport = true, + sortMenuShown = false, + sortOrder = BookmarksListSortOrder.default, + recursiveSelectedCount = null, + currentFolder = BookmarkItem.Folder( + guid = BookmarkRoot.Mobile.id, + title = "Bookmarks", + position = null, + ), + isSignedIntoSync = false, + bookmarksEditBookmarkState = null, + bookmarksAddFolderState = BookmarksAddFolderState( + parent = BookmarkItem.Folder( + guid = BookmarkRoot.Mobile.id, + title = "Bookmarks", + position = null, + ), + folderBeingAddedTitle = "Edit me!", + ), + openTabsConfirmationDialog = OpenTabsConfirmationDialog.None, + bookmarksDeletionDialogState = DeletionDialogState.None, + bookmarksSnackbarState = BookmarksSnackbarState.None, + bookmarksEditFolderState = null, + bookmarksSelectFolderState = BookmarksSelectFolderState( + outerSelectionGuid = "", + innerSelectionGuid = "guid1", + folders = listOf( + SelectFolderItem( + indentation = PREVIEW_INDENTATION_0, + folder = BookmarkItem.Folder("Bookmarks", "guid0", null), + expansionState = SelectFolderExpansionState.Closed, + ), + SelectFolderItem( + indentation = PREVIEW_INDENTATION_0, + folder = BookmarkItem.Folder("Bookmarks Menu", BookmarkRoot.Menu.id, null), + expansionState = SelectFolderExpansionState.None, + ), + SelectFolderItem( + indentation = PREVIEW_INDENTATION_0, + folder = BookmarkItem.Folder("Bookmarks Toolbar", BookmarkRoot.Toolbar.id, position = null), + expansionState = SelectFolderExpansionState.None, + ), + SelectFolderItem( + indentation = PREVIEW_INDENTATION_1, + folder = BookmarkItem.Folder("Desktop Bookmarks", BookmarkRoot.Root.id, position = null), + expansionState = SelectFolderExpansionState.None, + ), + SelectFolderItem( + indentation = PREVIEW_INDENTATION_0, + folder = BookmarkItem.Folder("Bookmarks Unfiled", BookmarkRoot.Unfiled.id, position = null), + expansionState = SelectFolderExpansionState.Open( + listOf( + SelectFolderItem( + indentation = PREVIEW_INDENTATION_1, + folder = BookmarkItem.Folder("Nested One", "guid0", position = null), + expansionState = SelectFolderExpansionState.Open( + listOf( + SelectFolderItem( + indentation = PREVIEW_INDENTATION_2, + folder = BookmarkItem.Folder("Nested Two", "guid0", position = null), + expansionState = SelectFolderExpansionState.None, + ), + SelectFolderItem( + indentation = PREVIEW_INDENTATION_2, + folder = BookmarkItem.Folder("Nested Two", "guid0", position = null), + expansionState = SelectFolderExpansionState.None, + ), + ), + ), + ), + SelectFolderItem( + indentation = PREVIEW_INDENTATION_1, + folder = BookmarkItem.Folder("Nested One", "guid0", position = null), + expansionState = SelectFolderExpansionState.Open( + listOf( + SelectFolderItem( + indentation = PREVIEW_INDENTATION_2, + folder = BookmarkItem.Folder("Nested Two", "guid1", position = null), + expansionState = SelectFolderExpansionState.Open( + listOf( + SelectFolderItem( + indentation = PREVIEW_INDENTATION_3, + folder = BookmarkItem.Folder( + title = "Nested Three", + guid = "guid0", + position = null, + ), + expansionState = SelectFolderExpansionState.None, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + bookmarksMultiselectMoveState = null, + isLoading = false, + isSearching = false, + ), + ) + FirefoxTheme { + SelectFolderScreen(modifier = Modifier, store = store) + } +}