For #26908 - Update wallpapers settings page design

pull/543/head
sarah541 2 years ago committed by mergify[bot]
parent 29e871086b
commit 805fb0ff60

@ -16,6 +16,7 @@ import androidx.annotation.IdRes
enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromGlobal(0),
FromHome(R.id.homeFragment),
FromWallpaper(R.id.wallpaperSettingsFragment),
FromSearchDialog(R.id.searchDialogFragment),
FromSettings(R.id.settingsFragment),
FromBookmarks(R.id.bookmarkFragment),

@ -121,6 +121,7 @@ import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirecti
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
@ -811,6 +812,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
NavGraphDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHome ->
HomeFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromWallpaper ->
WallpaperSettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearchDialog ->
SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSettings ->

@ -0,0 +1,27 @@
/* 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.settings.wallpaper
import org.mozilla.fenix.wallpapers.Wallpaper
/**
* The extension function to group wallpapers according to their name.
**/
fun List<Wallpaper>.groupByDisplayableCollection(): Map<Wallpaper.Collection, List<Wallpaper>> = groupBy {
it.collection
}.filter {
it.key.name != "default"
}.map {
val wallpapers = it.value.filter { wallpaper ->
wallpaper.thumbnailFileState == Wallpaper.ImageFileState.Downloaded
}
if (it.key.name == "classic-firefox") {
it.key to listOf(Wallpaper.Default) + wallpapers
} else {
it.key to wallpapers
}
}.toMap().takeIf {
it.isNotEmpty()
} ?: mapOf(Wallpaper.DefaultCollection to listOf(Wallpaper.Default))

@ -16,8 +16,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
@ -25,6 +27,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -37,9 +40,11 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ClickableSubstringLink
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.Wallpaper
@ -48,34 +53,110 @@ import org.mozilla.fenix.wallpapers.Wallpaper
* The screen for controlling settings around Wallpapers. When a new wallpaper is selected,
* a snackbar will be displayed.
*
* @param wallpapers Wallpapers to add to grid.
* @param wallpaperGroups Wallpapers groups to add to grid.
* @param selectedWallpaper The currently selected wallpaper.
* @param defaultWallpaper The default wallpaper.
* @param loadWallpaperResource Callback to handle loading a wallpaper bitmap. Only optional in the default case.
* @param onSelectWallpaper Callback for when a new wallpaper is selected.
* @param onLearnMoreClick Callback for when the learn more action is clicked from the group description.
*/
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
@Suppress("LongParameterList")
fun WallpaperSettings(
wallpapers: List<Wallpaper>,
wallpaperGroups: Map<Wallpaper.Collection, List<Wallpaper>>,
defaultWallpaper: Wallpaper,
loadWallpaperResource: suspend (Wallpaper) -> Bitmap?,
selectedWallpaper: Wallpaper,
onSelectWallpaper: (Wallpaper) -> Unit,
onLearnMoreClick: (String) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.background(color = FirefoxTheme.colors.layer1),
.background(color = FirefoxTheme.colors.layer1)
.padding(
end = 16.dp,
start = 16.dp,
top = 16.dp,
),
) {
WallpaperThumbnails(
wallpapers = wallpapers,
defaultWallpaper = defaultWallpaper,
loadWallpaperResource = loadWallpaperResource,
selectedWallpaper = selectedWallpaper,
onSelectWallpaper = { updatedWallpaper -> onSelectWallpaper(updatedWallpaper) },
wallpaperGroups.forEach { (collection, wallpapers) ->
if (wallpapers.isNotEmpty()) {
WallpaperGroupHeading(
collection = collection,
onLearnMoreClick = onLearnMoreClick,
)
Spacer(modifier = Modifier.height(16.dp))
WallpaperThumbnails(
wallpapers = wallpapers,
defaultWallpaper = defaultWallpaper,
loadWallpaperResource = loadWallpaperResource,
selectedWallpaper = selectedWallpaper,
onSelectWallpaper = onSelectWallpaper,
)
Spacer(modifier = Modifier.height(32.dp))
}
}
}
}
@Composable
private fun WallpaperGroupHeading(
collection: Wallpaper.Collection,
onLearnMoreClick: (String) -> Unit,
) {
// Since the last new collection of wallpapers was tied directly to an MR release,
// it was decided that we should use string resources for these titles
// and descriptions so they could be localized.
// In the future, we may want to either use the dynamic wallpaper properties with localized fallbacks
// or invest in a method of localizing the remote strings themselves.
if (collection.name == "classic-firefox") {
Text(
text = stringResource(R.string.wallpaper_classic_title),
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.subtitle2,
)
} else {
Column {
Text(
text = stringResource(R.string.wallpaper_limited_edition_title),
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.subtitle2,
)
Spacer(modifier = Modifier.height(2.dp))
if (collection.learnMoreUrl.isNullOrEmpty()) {
val text = stringResource(R.string.wallpaper_limited_edition_description)
Text(
text = text,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.caption,
)
} else {
val link = stringResource(R.string.wallpaper_learn_more)
val text = stringResource(R.string.wallpaper_limited_edition_description_with_learn_more, link)
val linkStartIndex = text.indexOf(link)
val linkEndIndex = linkStartIndex + link.length
ClickableSubstringLink(
text = text,
textColor = FirefoxTheme.colors.textSecondary,
linkTextColor = FirefoxTheme.colors.textSecondary,
linkTextDecoration = TextDecoration.Underline,
clickableStartIndex = linkStartIndex,
clickableEndIndex = linkEndIndex,
) {
onLearnMoreClick(collection.learnMoreUrl)
}
}
}
}
}
@ -88,8 +169,6 @@ fun WallpaperSettings(
* @param selectedWallpaper The currently selected wallpaper.
* @param numColumns The number of columns that will occupy the grid.
* @param onSelectWallpaper Action to take when a new wallpaper is selected.
* @param verticalPadding Vertical content padding inside the block.
* @param horizontalPadding Horizontal content padding inside the block.
*/
@Composable
@Suppress("LongParameterList")
@ -100,39 +179,30 @@ fun WallpaperThumbnails(
loadWallpaperResource: suspend (Wallpaper) -> Bitmap?,
onSelectWallpaper: (Wallpaper) -> Unit,
numColumns: Int = 3,
verticalPadding: Int = 30,
horizontalPadding: Int = 20,
) {
Column(
modifier = Modifier.padding(
vertical = verticalPadding.dp,
horizontal = horizontalPadding.dp,
),
) {
val numRows = (wallpapers.size + numColumns - 1) / numColumns
for (rowIndex in 0 until numRows) {
Row {
for (columnIndex in 0 until numColumns) {
val itemIndex = rowIndex * numColumns + columnIndex
if (itemIndex < wallpapers.size) {
val wallpaper = wallpapers[itemIndex]
Box(
modifier = Modifier
.weight(1f, fill = true)
.padding(4.dp),
) {
WallpaperThumbnailItem(
wallpaper = wallpaper,
defaultWallpaper = defaultWallpaper,
loadWallpaperResource = loadWallpaperResource,
isSelected = selectedWallpaper.name == wallpaper.name,
isLoading = wallpaper.assetsFileState == Wallpaper.ImageFileState.Downloading,
onSelect = onSelectWallpaper,
)
}
} else {
Spacer(Modifier.weight(1f))
val numRows = (wallpapers.size + numColumns - 1) / numColumns
for (rowIndex in 0 until numRows) {
Row {
for (columnIndex in 0 until numColumns) {
val itemIndex = rowIndex * numColumns + columnIndex
if (itemIndex < wallpapers.size) {
val wallpaper = wallpapers[itemIndex]
Box(
modifier = Modifier
.weight(1f, fill = true)
.padding(4.dp),
) {
WallpaperThumbnailItem(
wallpaper = wallpaper,
defaultWallpaper = defaultWallpaper,
loadWallpaperResource = loadWallpaperResource,
isSelected = selectedWallpaper.name == wallpaper.name,
isLoading = wallpaper.assetsFileState == Wallpaper.ImageFileState.Downloading,
onSelect = onSelectWallpaper,
)
}
} else {
Spacer(Modifier.weight(1f))
}
}
}
@ -229,9 +299,10 @@ private fun WallpaperThumbnailsPreview() {
WallpaperSettings(
defaultWallpaper = Wallpaper.Default,
loadWallpaperResource = { null },
wallpapers = listOf(Wallpaper.Default),
wallpaperGroups = mapOf(Wallpaper.DefaultCollection to listOf(Wallpaper.Default)),
selectedWallpaper = Wallpaper.Default,
onSelectWallpaper = {},
onLearnMoreClick = {},
)
}
}

@ -17,7 +17,9 @@ import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsComposableState
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Wallpapers
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.requireComponents
@ -54,7 +56,7 @@ class WallpaperSettingsFragment : Fragment() {
val coroutineScope = rememberCoroutineScope()
WallpaperSettings(
wallpapers = wallpapers,
wallpaperGroups = wallpapers.groupByDisplayableCollection(),
defaultWallpaper = Wallpaper.Default,
selectedWallpaper = currentWallpaper,
loadWallpaperResource = {
@ -66,6 +68,13 @@ class WallpaperSettingsFragment : Fragment() {
onWallpaperSelected(it, result, this@apply)
}
},
onLearnMoreClick = { url ->
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = url,
newTab = true,
from = BrowserDirection.FromWallpaper,
)
},
)
}
}

@ -91,16 +91,18 @@ fun WallpaperOnboarding(
style = FirefoxTheme.typography.caption,
)
Spacer(modifier = Modifier.height(16.dp))
WallpaperThumbnails(
wallpapers = wallpapers,
defaultWallpaper = Wallpaper.Default,
selectedWallpaper = currentWallpaper,
loadWallpaperResource = { loadWallpaperResource(it) },
onSelectWallpaper = { onSelectWallpaper(it) },
verticalPadding = 16,
horizontalPadding = 0,
)
Spacer(modifier = Modifier.height(16.dp))
TextButton(
modifier = Modifier
.align(Alignment.CenterHorizontally)

@ -459,12 +459,21 @@
<string name="wallpaper_download_error_snackbar_action">Try again</string>
<!-- Snackbar message for when wallpaper couldn't be selected because of the disk error -->
<string name="wallpaper_select_error_snackbar_message">Couldnt change wallpaper</string>
<!-- Text displayed that links to website containing documentation about the "Limited Edition" wallpapers. -->
<string name="wallpaper_learn_more">Learn more</string>
<!-- Label for switch which toggles the "tap-to-switch" behavior on home screen logo -->
<string name="wallpaper_tap_to_change_switch_label_1" moz:removedIn="105" tools:ignore="UnusedResources">Change wallpaper by tapping Firefox homepage logo</string>
<!-- This is the accessibility content description for the wallpapers functionality. Users are
able to tap on the app logo in the home screen and can switch to different wallpapers by tapping. -->
<string name="wallpaper_logo_content_description" moz:removedIn="105" tools:ignore="UnusedResources">Firefox logo - change the wallpaper, button</string>
<!-- Text for classic wallpapers title. -->
<string name="wallpaper_classic_title">Classic Firefox</string>
<!-- Text for limited edition wallpapers title. -->
<string name="wallpaper_limited_edition_title">Limited Edition</string>
<!-- Description text for the limited edition wallpapers with learn more link. The first parameter is the learn more string defined in wallpaper_learn_more-->
<string name="wallpaper_limited_edition_description_with_learn_more">The new Independent Voices collection. %s</string>
<!-- Description text for the limited edition wallpapers. -->
<string name="wallpaper_limited_edition_description">The new Independent Voices collection.</string>
<!-- Wallpaper onboarding dialog header text. -->
<string name="wallpapers_onboarding_dialog_title_text">Try a splash of color</string>
<!-- Wallpaper onboarding dialog body text. -->

@ -0,0 +1,88 @@
/* 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.settings.wallpaper
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.wallpapers.Wallpaper
class ExtensionsTest {
private val classicCollection = getSeasonalCollection("classic-firefox")
@Test
fun `GIVEN wallpapers that include the default WHEN grouped by collection THEN default will be added to classic firefox`() {
val seasonalCollection = getSeasonalCollection("finally fall")
val classicFirefoxWallpapers = (0..5).map { generateClassicFirefoxWallpaper("firefox$it") }
val seasonalWallpapers = (0..5).map { generateSeasonalWallpaperCollection("${seasonalCollection.name}$it", seasonalCollection.name) }
val allWallpapers = listOf(Wallpaper.Default) + classicFirefoxWallpapers + seasonalWallpapers
val result = allWallpapers.groupByDisplayableCollection()
assertEquals(2, result.size)
assertEquals(listOf(Wallpaper.Default) + classicFirefoxWallpapers, result[classicCollection])
assertEquals(seasonalWallpapers, result[seasonalCollection])
}
@Test
fun `GIVEN no wallpapers but the default WHEN grouped by collection THEN the default will still be present`() {
val result = listOf(Wallpaper.Default).groupByDisplayableCollection()
assertEquals(1, result.size)
assertEquals(listOf(Wallpaper.Default), result[Wallpaper.DefaultCollection])
}
@Test
fun `GIVEN wallpapers with thumbnails that have not downloaded WHEN grouped by collection THEN wallpapers without thumbnails will not be included`() {
val seasonalCollection = getSeasonalCollection("finally fall")
val classicFirefoxWallpapers = (0..5).map { generateClassicFirefoxWallpaper("firefox$it") }
val downloadedSeasonalWallpapers = (0..5).map { generateSeasonalWallpaperCollection("${seasonalCollection.name}$it", seasonalCollection.name) }
val nonDownloadedSeasonalWallpapers = (0..5).map {
generateSeasonalWallpaperCollection(
"${seasonalCollection.name}$it",
seasonalCollection.name,
Wallpaper.ImageFileState.Error,
)
}
val allWallpapers = listOf(Wallpaper.Default) + classicFirefoxWallpapers + downloadedSeasonalWallpapers + nonDownloadedSeasonalWallpapers
val result = allWallpapers.groupByDisplayableCollection()
assertEquals(2, result.size)
assertEquals(listOf(Wallpaper.Default) + classicFirefoxWallpapers, result[classicCollection])
assertEquals(downloadedSeasonalWallpapers, result[seasonalCollection])
}
private fun generateClassicFirefoxWallpaper(name: String) = Wallpaper(
name = name,
textColor = 0L,
cardColor = 0L,
thumbnailFileState = Wallpaper.ImageFileState.Downloaded,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
collection = classicCollection,
)
private fun getSeasonalCollection(name: String) = Wallpaper.Collection(
name = name,
heading = null,
description = null,
learnMoreUrl = null,
availableLocales = null,
startDate = null,
endDate = null,
)
private fun generateSeasonalWallpaperCollection(
wallpaperName: String,
collectionName: String,
thumbnailState: Wallpaper.ImageFileState = Wallpaper.ImageFileState.Downloaded,
) = Wallpaper(
name = wallpaperName,
textColor = 0L,
cardColor = 0L,
thumbnailFileState = thumbnailState,
assetsFileState = Wallpaper.ImageFileState.Downloaded,
collection = getSeasonalCollection(collectionName),
)
}
Loading…
Cancel
Save