Divested-Mobile / Mull-Fenix

Build scripts for a web browser built upon Mozilla technology
https://divestos.org/pages/our_apps#mull
GNU Affero General Public License v3.0
611 stars 16 forks source link

Change menu UI #59

Closed basings closed 2 years ago

basings commented 2 years ago

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.

Screenshot_20220422_151302

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?

SkewedZeppelin commented 2 years ago

While that is neat and a good job, such changes are out of scope for this project, try to convince upstream.

basings commented 2 years ago

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.

Bugaddr commented 2 years ago

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.

basings commented 2 years ago

@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?

basings commented 2 years ago
/* 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
        }
    }
}
Bugaddr commented 2 years ago

@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?

Thanks @basings i appreciate your efforts, above file is enough :)