Closed basings closed 2 years ago
While that is neat and a good job, such changes are out of scope for this project, try to convince upstream.
Ihighly doubt that fenix is interested. It's not as beginner/ stupid friendly as the current implementation but I'll suggest it, thx for thinking about it.
Ihighly doubt that fenix is interested. It's not as beginner/ stupid friendly as the current implementation but I'll suggest it, thx for thinking about it.
Hey, can you share this as a patch ? It looks very neat.
@Jrchintu Yes, of course.
I did not polish the code since noone was interested in it. All code changes are within > components > toolbar > DefaultToolbarMenu.kt
It's an alpha version, only for bottom toolbar and isn't available on the new tab page. There are 5 new variables downloadsItem2, historyItem2, syncMenuItem2, settingsItem2, quitt
which are put into coreMenuItems
. That's all.
I'll comment the file below, or how would you like it to be shared?
/* 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.components.toolbar
import android.content.Context
import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.core.content.ContextCompat.getColor
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenuHighlight
import mozilla.components.browser.menu.WebExtensionBrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuImageTextCheckboxButton
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.TwoStateBrowserMenuImageText
import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.feature.top.sites.PinnedSiteStorage
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.theme.ThemeManager
/**
* Builds the toolbar object used with the 3-dot menu in the browser fragment.
* @param store reference to the application's [BrowserStore].
* @param hasAccountProblem If true, there was a problem signing into the Firefox account.
* @param shouldReverseItems If true, reverse the menu items.
* @param pinnedSiteStorage Used to check if the current url is a pinned site.
* @param onItemTapped Called when a menu item is tapped.
* @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
* @param bookmarksStorage Used to check if a page is bookmarked.
*/
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
open class DefaultToolbarMenu(
private val context: Context,
private val store: BrowserStore,
hasAccountProblem: Boolean = false,
private val onItemTapped: (ToolbarMenu.Item) -> Unit = {},
private val lifecycleOwner: LifecycleOwner,
private val bookmarksStorage: BookmarksStorage,
private val pinnedSiteStorage: PinnedSiteStorage,
val isPinningSupported: Boolean
) : ToolbarMenu {
private var isCurrentUrlPinned = false
private var isCurrentUrlBookmarked = false
private var isBookmarkedJob: Job? = null
private val shouldDeleteDataOnQuit = context.settings().shouldDeleteBrowsingDataOnQuit
private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar
private val accountManager = FenixAccountManager(context)
private val selectedSession: TabSessionState?
get() = store.state.selectedTab
override val menuBuilder by lazy {
WebExtensionBrowserMenuBuilder(
items = coreMenuItems,
endOfMenuAlwaysVisible = shouldUseBottomToolbar,
store = store,
style = WebExtensionBrowserMenuBuilder.Style(
webExtIconTintColorResource = primaryTextColor(),
addonsManagerMenuItemDrawableRes = R.drawable.ic_addons_extensions
),
onAddonsManagerTapped = {
onItemTapped.invoke(ToolbarMenu.Item.AddonsManager)
},
appendExtensionSubMenuAtStart = shouldUseBottomToolbar
)
}
override val menuToolbar by lazy {
val back = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
selectedSession?.content?.canGoBack ?: true
},
secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.textDisabled, context),
disableInSecondaryState = true,
longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Back(viewHistory = true)) }
) {
onItemTapped.invoke(ToolbarMenu.Item.Back(viewHistory = false))
}
val forward = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
selectedSession?.content?.canGoForward ?: true
},
secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.textDisabled, context),
disableInSecondaryState = true,
longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = true)) }
) {
onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = false))
}
val refresh = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_refresh,
primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = primaryTextColor(),
isInPrimaryState = {
selectedSession?.content?.loading == false
},
secondaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_stop,
secondaryContentDescription = context.getString(R.string.browser_menu_stop),
secondaryImageTintResource = primaryTextColor(),
disableInSecondaryState = false,
longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = true)) }
) {
if (selectedSession?.content?.loading == true) {
onItemTapped.invoke(ToolbarMenu.Item.Stop)
} else {
onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = false))
}
}
val share = BrowserMenuItemToolbar.Button(
imageResource = R.drawable.ic_share,
contentDescription = context.getString(R.string.browser_menu_share),
iconTintColorResource = primaryTextColor(),
listener = {
onItemTapped.invoke(ToolbarMenu.Item.Share)
}
)
registerForIsBookmarkedUpdates()
BrowserMenuItemToolbar(listOf(back, forward, share, refresh), isSticky = true)
}
// Predicates that need to be repeatedly called as the session changes
@VisibleForTesting(otherwise = PRIVATE)
fun canAddToHomescreen(): Boolean =
selectedSession != null && isPinningSupported &&
!context.components.useCases.webAppUseCases.isInstallable()
@VisibleForTesting(otherwise = PRIVATE)
fun canInstall(): Boolean =
selectedSession != null && isPinningSupported &&
context.components.useCases.webAppUseCases.isInstallable()
@VisibleForTesting(otherwise = PRIVATE)
fun shouldShowOpenInApp(): Boolean = selectedSession?.let { session ->
val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect
appLink(session.content.url).hasExternalApp()
} ?: false
@VisibleForTesting(otherwise = PRIVATE)
fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active
} ?: false
// End of predicates //
val installToHomescreen = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_install_on_homescreen),
startImageResource = R.drawable.mozac_ic_add_to_home_screen,
iconTintColorResource = primaryTextColor(),
highlight = BrowserMenuHighlight.LowPriority(
label = context.getString(R.string.browser_menu_install_on_homescreen),
notificationTint = getColor(context, R.color.fx_mobile_icon_color_information)
),
isHighlighted = {
!context.settings().installPwaOpened
}
) {
onItemTapped.invoke(ToolbarMenu.Item.InstallPwaToHomeScreen)
}
val newTabItem = BrowserMenuImageText(
context.getString(R.string.library_new_tab),
R.drawable.ic_new,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.NewTab)
}
val historyItem = BrowserMenuImageText(
context.getString(R.string.library_history),
R.drawable.ic_history,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.History)
}
val downloadsItem = BrowserMenuImageText(
context.getString(R.string.library_downloads),
R.drawable.ic_download,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
}
val extensionsItem = WebExtensionPlaceholderMenuItem(
id = WebExtensionPlaceholderMenuItem.MAIN_EXTENSIONS_MENU_ID
)
val findInPageItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
}
val desktopSiteItem = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site),
initialState = {
selectedSession?.content?.desktopMode ?: false
}
) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}
val customizeReaderView = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_customize_reader_view),
imageResource = R.drawable.ic_readermode_appearance,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.CustomizeReaderView)
}
val openInApp = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_open_app_link),
startImageResource = R.drawable.ic_open_in_app,
iconTintColorResource = primaryTextColor(),
highlight = BrowserMenuHighlight.LowPriority(
label = context.getString(R.string.browser_menu_open_app_link),
notificationTint = getColor(context, R.color.fx_mobile_icon_color_information)
),
isHighlighted = { !context.settings().openInAppOpened }
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
}
val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem(
id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID,
iconTintColorResource = primaryTextColor()
)
val addToHomeScreenItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_homescreen),
imageResource = R.drawable.mozac_ic_add_to_home_screen,
iconTintColorResource = primaryTextColor(),
isCollapsingMenuLimit = true
) {
onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen)
}
val addRemoveTopSitesItem = TwoStateBrowserMenuImageText(
primaryLabel = context.getString(R.string.browser_menu_add_to_shortcuts),
secondaryLabel = context.getString(R.string.browser_menu_remove_from_shortcuts),
primaryStateIconResource = R.drawable.ic_top_sites,
secondaryStateIconResource = R.drawable.ic_top_sites,
iconTintColorResource = primaryTextColor(),
isInPrimaryState = { !isCurrentUrlPinned },
isInSecondaryState = { isCurrentUrlPinned },
primaryStateAction = {
isCurrentUrlPinned = true
onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites)
},
secondaryStateAction = {
isCurrentUrlPinned = false
onItemTapped.invoke(ToolbarMenu.Item.RemoveFromTopSites)
}
)
val saveToCollectionItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_save_to_collection_2),
imageResource = R.drawable.ic_tab_collection,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
}
val settingsItem = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
startImageResource = R.drawable.mozac_ic_settings,
iconTintColorResource = if (hasAccountProblem) {
ThemeManager.resolveAttribute(R.attr.syncDisconnected, context)
} else {
primaryTextColor()
},
textColorResource = if (hasAccountProblem) {
ThemeManager.resolveAttribute(R.attr.textPrimary, context)
} else {
primaryTextColor()
},
highlight = BrowserMenuHighlight.HighPriority(
endImageResource = R.drawable.ic_sync_disconnected,
backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground),
canPropagate = false
),
isHighlighted = { hasAccountProblem }
) {
onItemTapped.invoke(ToolbarMenu.Item.Settings)
}
val bookmarksItem = BrowserMenuImageTextCheckboxButton(
imageResource = R.drawable.ic_bookmarks_menu,
iconTintColorResource = primaryTextColor(),
label = context.getString(R.string.library_bookmarks),
labelListener = {
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
},
primaryStateIconResource = R.drawable.ic_bookmark_outline,
secondaryStateIconResource = R.drawable.ic_bookmark_filled,
tintColorResource = menuItemButtonTintColor(),
primaryLabel = context.getString(R.string.browser_menu_add),
secondaryLabel = context.getString(R.string.browser_menu_edit),
isInPrimaryState = { !isCurrentUrlBookmarked }
) {
handleBookmarkItemTapped()
}
val deleteDataOnQuit = BrowserMenuImageText(
label = context.getString(R.string.delete_browsing_data_on_quit_action),
imageResource = R.drawable.mozac_ic_quit,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Quit)
}
private fun getSyncItemTitle() =
accountManager.accountProfileEmail ?: context.getString(R.string.sync_menu_sign_in)
val syncMenuItem = BrowserMenuImageText(
getSyncItemTitle(),
R.drawable.ic_signed_out,
primaryTextColor()
) {
onItemTapped.invoke(
ToolbarMenu.Item.SyncAccount(accountManager.accountState)
)
}
val historyItem2 = BrowserMenuItemToolbar.Button(
imageResource = R.drawable.ic_history,
contentDescription = context.getString(R.string.library_history),
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.History)
}
val downloadsItem2 = BrowserMenuItemToolbar.Button(
contentDescription = context.getString(R.string.library_downloads),
imageResource = R.drawable.ic_download,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
}
val syncMenuItem2 = BrowserMenuItemToolbar.Button(
contentDescription = getSyncItemTitle(),
imageResource = R.drawable.ic_signed_out,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(
ToolbarMenu.Item.SyncAccount(accountManager.accountState)
)
}
// MINE
val quitt = BrowserMenuItemToolbar.Button(
imageResource = R.drawable.mozac_ic_quit,
contentDescription = context.getString(R.string.delete_browsing_data_on_quit_action),
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Quit)
}
val settingsItem2 = BrowserMenuItemToolbar.Button(
imageResource = R.drawable.mozac_ic_settings,
contentDescription = "settings",
iconTintColorResource = if (hasAccountProblem) {
ThemeManager.resolveAttribute(R.attr.syncDisconnected, context)
} else {
primaryTextColor()
},
) {
onItemTapped.invoke(ToolbarMenu.Item.Settings)
}
@VisibleForTesting(otherwise = PRIVATE)
val coreMenuItems by lazy {
val defaultBrowserItem = getSetDefaultBrowserItem()
val menuItems =
listOfNotNull(
// if (shouldUseBottomToolbar) null else menuToolbar,
//newTabItem,
//historyItem,
//downloadsItem,
desktopSiteItem,
extensionsItem,
bookmarksItem,
// BrowserMenuDivider(),
defaultBrowserItem,
// defaultBrowserItem?.let { BrowserMenuDivider() },
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },
openInApp.apply { visible = ::shouldShowOpenInApp },
// BrowserMenuItemToolbar(listOf(, )),
addToHomeScreenItem.apply { visible = ::canAddToHomescreen },
installToHomescreen.apply { visible = ::canInstall },
//syncMenuItem,
reportSiteIssuePlaceholder,
//addRemoveTopSitesItem,
//saveToCollectionItem,
//BrowserMenuDivider(),
//if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
//if (shouldUseBottomToolbar) BrowserMenuDivider() else null,
BrowserMenuItemToolbar(listOf(downloadsItem2, historyItem2, syncMenuItem2, settingsItem2, quitt)),
//if (shouldUseBottomToolbar) BrowserMenuDivider() else null,
//if (shouldUseBottomToolbar) menuToolbar else null
menuToolbar
)
menuItems
}
private fun handleBookmarkItemTapped() {
if (!isCurrentUrlBookmarked) isCurrentUrlBookmarked = true
onItemTapped.invoke(ToolbarMenu.Item.Bookmark)
}
@ColorRes
@VisibleForTesting
internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.textPrimary, context)
@ColorRes
@VisibleForTesting
internal fun menuItemButtonTintColor() = ThemeManager.resolveAttribute(R.attr.menuItemButtonTintColor, context)
@VisibleForTesting
internal fun updateIsCurrentUrlPinned(currentUrl: String) {
lifecycleOwner.lifecycleScope.launch {
isCurrentUrlPinned = pinnedSiteStorage
.getPinnedSites()
.find { it.url == currentUrl } != null
}
}
@VisibleForTesting
internal fun registerForIsBookmarkedUpdates() {
store.flowScoped(lifecycleOwner) { flow ->
flow.mapNotNull { state -> state.selectedTab }
.ifAnyChanged { tab ->
arrayOf(
tab.id,
tab.content.url
)
}
.collect {
isCurrentUrlPinned = false
updateIsCurrentUrlPinned(it.content.url)
isCurrentUrlBookmarked = false
updateCurrentUrlIsBookmarked(it.content.url)
}
}
}
@VisibleForTesting
internal fun updateCurrentUrlIsBookmarked(newUrl: String) {
isBookmarkedJob?.cancel()
isBookmarkedJob = lifecycleOwner.lifecycleScope.launch {
isCurrentUrlBookmarked = bookmarksStorage
.getBookmarksWithUrl(newUrl)
.any { it.url == newUrl }
}
}
private fun getSetDefaultBrowserItem(): BrowserMenuImageText? {
val settings = context.components.settings
return if (
settings.isDefaultBrowserMessageLocation(MessageSurfaceId.APP_MENU_ITEM)
) {
BrowserMenuImageText(
label = context.getString(R.string.preferences_set_as_default_browser),
imageResource = R.mipmap.ic_launcher
) {
onItemTapped.invoke(ToolbarMenu.Item.SetDefaultBrowser)
}
} else {
null
}
}
}
@Jrchintu Yes, of course.
I did not polish the code since noone was interested in it. All code changes are within
> components > toolbar > DefaultToolbarMenu.kt
It's an alpha version, only for bottom toolbar and isn't available on the new tab page. There are 5 new variables
downloadsItem2, historyItem2, syncMenuItem2, settingsItem2, quitt
which are put intocoreMenuItems
. That's all.I'll comment the file below, or how would you like it to be shared?
Thanks @basings i appreciate your efforts, above file is enough :)
Hey,
I just wanted to ask if you'd be interested in a little UI change.
I was a little bit annoyed that some settings weren't easily and very quickly available in firefox and mull. I just wanted to ask if you'd be interested in including the changes in mull. I actually wanted to see if I can create a bottom toolbar for quick access of features. I am not an android dev so I took the easiest route I could find and the result is shown below.
There's still inconsistency in the codebase for the homepage and something else. It works for me , so I wouldn't change anything else if the changes won't go public but if you'd be interested, I could polish it further. So far all changes are within one file and are only minor and don't affect anything else. Meaning no complications. I'd just wanted to know if you'd be down for an ui change?