[#948] Updated Settings screen UI

* [#948] Updated Settings screen UI

* [#948] Crash fix on settings screen

* Center button UI components text

So the text aligns with the center when the button is displayed on a very small device or, e.g., when the system accessibility text size increase feature is used.

* Keep button spacing

When rotated to landscape, the weighted spacing disappears. But we ideally want buttons to have all the same spacing around them in this scenario.

* Kotlin code style

* Move previous settings option to overflow

- As the new design prescribes no debug/test options in the settings screen, we moved these useful actions into the only troubleshooting menu in the app bar.
- Tests added

* Add UI tests

* Text in all caps in the components

Rather than enforcing all caps in translations.

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Venkat-corebts 2023-09-28 17:47:21 +05:30 committed by GitHub
parent 3b1d42528d
commit 705386e031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 603 additions and 192 deletions

View File

@ -9,8 +9,7 @@
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
@ -126,6 +125,7 @@
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -79,7 +80,12 @@ fun PrimaryButton(
),
onClick = onClick,
) {
Text(text = text, color = textColor, style = MaterialTheme.typography.labelLarge)
Text(
style = ZcashTheme.extendedTypography.buttonText,
textAlign = TextAlign.Center,
text = text.uppercase(),
color = textColor
)
}
}
@ -107,7 +113,8 @@ fun SecondaryButton(
) {
Text(
style = MaterialTheme.typography.labelLarge,
text = text,
textAlign = TextAlign.Center,
text = text.uppercase(),
color = MaterialTheme.colorScheme.onSecondary
)
}
@ -132,7 +139,12 @@ fun NavigationButton(
),
colors = buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
) {
Text(style = MaterialTheme.typography.labelLarge, text = text, color = MaterialTheme.colorScheme.onSecondary)
Text(
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
text = text,
color = MaterialTheme.colorScheme.onSecondary
)
}
}
@ -161,6 +173,7 @@ fun TertiaryButton(
) {
Text(
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
text = text,
color = ZcashTheme.colors.onTertiary
)
@ -189,6 +202,7 @@ fun DangerousButton(
) {
Text(
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
text = text,
color = ZcashTheme.colors.onDangerous
)

View File

@ -30,6 +30,7 @@ data class ExtendedColors(
val disabledButtonColor: Color,
val disabledButtonTextColor: Color,
val buttonShadowColor: Color,
val screenTitleColor: Color,
) {
@Composable
fun surfaceGradient() = Brush.verticalGradient(

View File

@ -59,6 +59,9 @@ internal object Dark {
val disabledButtonTextColor = Color(0xFFDDDDDD)
val buttonShadowColor = Color(0xFFFFFFFF)
// to be added later for dark theme
val screenTitleColor = Color.Unspecified
}
internal object Light {
@ -115,6 +118,8 @@ internal object Light {
val disabledButtonColor = Color(0xFFB7B7B7)
val disabledButtonTextColor = Color(0xFFDDDDDD)
val buttonShadowColor = Color(0xFF000000)
val screenTitleColor = Color(0xFF040404)
}
internal val DarkColorPalette = darkColorScheme(
@ -162,7 +167,7 @@ internal val DarkExtendedColorPalette = ExtendedColors(
disabledButtonColor = Dark.disabledButtonColor,
reference = Dark.reference,
buttonShadowColor = Dark.buttonShadowColor,
screenTitleColor = Dark.screenTitleColor
)
internal val LightExtendedColorPalette = ExtendedColors(
@ -187,7 +192,8 @@ internal val LightExtendedColorPalette = ExtendedColors(
disabledButtonTextColor = Light.disabledButtonTextColor,
disabledButtonColor = Light.disabledButtonColor,
reference = Light.reference,
buttonShadowColor = Light.buttonShadowColor
buttonShadowColor = Light.buttonShadowColor,
screenTitleColor = Light.screenTitleColor,
)
@Suppress("CompositionLocalAllowlist")
@ -215,5 +221,6 @@ internal val LocalExtendedColors = staticCompositionLocalOf {
disabledButtonColor = Color.Unspecified,
reference = Color.Unspecified,
buttonShadowColor = Color.Unspecified,
screenTitleColor = Color.Unspecified,
)
}

View File

@ -49,6 +49,11 @@ internal val PrimaryTypography = Typography(
fontWeight = FontWeight.SemiBold,
fontSize = 30.sp
),
titleSmall = TextStyle(
fontFamily = InterFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
),
bodyLarge = TextStyle(
fontFamily = InterFontFamily,
fontWeight = FontWeight.Normal,
@ -99,7 +104,8 @@ data class Typography(
data class ExtendedTypography(
val chipIndex: TextStyle,
val listItem: TextStyle,
val zecBalance: TextStyle
val zecBalance: TextStyle,
val buttonText: TextStyle,
)
@Suppress("CompositionLocalAllowlist")
@ -125,6 +131,9 @@ val LocalExtendedTypography = staticCompositionLocalOf {
fontFamily = Zboto,
fontWeight = FontWeight.Normal,
fontSize = 30.sp
)
),
buttonText = PrimaryTypography.bodySmall.copy(
fontSize = 14.sp
),
)
}

View File

@ -0,0 +1,113 @@
package co.electriccoin.zcash.ui.screen.settings
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.screen.settings.view.Settings
import java.util.concurrent.atomic.AtomicInteger
class SettingsViewTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val troubleshootingParameters: TroubleshootingParameters
) {
private val onBackCount = AtomicInteger(0)
private val onBackupCount = AtomicInteger(0)
private val onDocumentationCount = AtomicInteger(0)
private val onPrivacyPolicyCount = AtomicInteger(0)
private val onFeedbackCount = AtomicInteger(0)
private val onAboutCount = AtomicInteger(0)
private val onRescanCount = AtomicInteger(0)
private val onBackgroundSyncChangedCount = AtomicInteger(0)
private val onKeepScreenOnChangedCount = AtomicInteger(0)
private val onAnalyticsChangedCount = AtomicInteger(0)
fun getBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getBackupCount(): Int {
composeTestRule.waitForIdle()
return onBackupCount.get()
}
fun getDocumentationCount(): Int {
composeTestRule.waitForIdle()
return onDocumentationCount.get()
}
fun getPrivacyPolicyCount(): Int {
composeTestRule.waitForIdle()
return onPrivacyPolicyCount.get()
}
fun getFeedbackCount(): Int {
composeTestRule.waitForIdle()
return onFeedbackCount.get()
}
fun getAboutCount(): Int {
composeTestRule.waitForIdle()
return onAboutCount.get()
}
fun getRescanCount(): Int {
composeTestRule.waitForIdle()
return onRescanCount.get()
}
fun getBackgroundSyncCount(): Int {
composeTestRule.waitForIdle()
return onBackgroundSyncChangedCount.get()
}
fun getKeepScreenOnSyncCount(): Int {
composeTestRule.waitForIdle()
return onKeepScreenOnChangedCount.get()
}
fun getAnalyticsCount(): Int {
composeTestRule.waitForIdle()
return onAnalyticsChangedCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
Settings(
troubleshootingParameters = troubleshootingParameters,
onBack = {
onBackCount.incrementAndGet()
},
onBackup = {
onBackupCount.incrementAndGet()
},
onDocumentation = {
onDocumentationCount.incrementAndGet()
},
onPrivacyPolicy = {
onPrivacyPolicyCount.incrementAndGet()
},
onFeedback = {
onFeedbackCount.incrementAndGet()
},
onAbout = {
onAboutCount.incrementAndGet()
},
onRescanWallet = {
onRescanCount.incrementAndGet()
},
onBackgroundSyncSettingsChanged = {
onBackgroundSyncChangedCount.incrementAndGet()
},
onKeepScreenOnDuringSyncSettingsChanged = {
onKeepScreenOnChangedCount.incrementAndGet()
},
onAnalyticsSettingsChanged = {
onAnalyticsChangedCount.incrementAndGet()
}
)
}
}
}
}

View File

@ -0,0 +1,25 @@
package co.electriccoin.zcash.ui.screen.settings.fixture
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
internal object TroubleshootingParametersFixture {
internal const val ENABLED = false
internal const val BACKGROUND_SYNC_ENABLED = false
internal const val KEEP_SCREEN_ON_DURING_SYNC_ENABLED = false
internal const val ANALYTICS_ENABLED = false
internal const val RESCAN_ENABLED = false
fun new(
isEnabled: Boolean = ENABLED,
isBackgroundSyncEnabled: Boolean = BACKGROUND_SYNC_ENABLED,
isKeepScreenOnDuringSyncEnabled: Boolean = KEEP_SCREEN_ON_DURING_SYNC_ENABLED,
isAnalyticsEnabled: Boolean = ANALYTICS_ENABLED,
isRescanEnabled: Boolean = RESCAN_ENABLED,
) = TroubleshootingParameters(
isEnabled,
isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled,
isAnalyticsEnabled,
isRescanEnabled
)
}

View File

@ -3,34 +3,31 @@ package co.electriccoin.zcash.ui.screen.settings.view
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
import androidx.test.filters.SmallTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.settings.SettingsTag
import co.electriccoin.zcash.ui.screen.settings.SettingsViewTestSetup
import co.electriccoin.zcash.ui.screen.settings.fixture.TroubleshootingParametersFixture
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
@OptIn(ExperimentalCoroutinesApi::class)
class SettingsViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun back() = runTest {
val testSetup = TestSetup(composeTestRule)
fun on_back_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
assertEquals(0, testSetup.getOnBackCount())
assertEquals(0, testSetup.getBackCount())
composeTestRule.onNodeWithContentDescription(
getStringResource(R.string.settings_back_content_description)
@ -38,141 +35,208 @@ class SettingsViewTest : UiTestPrerequisites() {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
assertEquals(1, testSetup.getBackCount())
}
@Test
@MediumTest
fun rescan() = runTest {
val testSetup = TestSetup(composeTestRule)
fun on_feedback_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
if (ConfigurationEntries.IS_RESCAN_ENABLED.getValue(
StringConfiguration(emptyMap<String, String>().toPersistentMap(), null)
assertEquals(0, testSetup.getFeedbackCount())
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_send_us_feedback)
).also {
it.performClick()
}
assertEquals(1, testSetup.getFeedbackCount())
}
@Test
@MediumTest
fun on_backup_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
assertEquals(0, testSetup.getBackupCount())
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_backup_wallet)
).also {
it.performClick()
}
assertEquals(1, testSetup.getBackupCount())
}
@Test
@MediumTest
fun on_documentation_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
assertEquals(0, testSetup.getDocumentationCount())
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_documentation)
).also {
it.performClick()
}
assertEquals(1, testSetup.getDocumentationCount())
}
@Test
@MediumTest
fun on_privacy_policy_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
assertEquals(0, testSetup.getPrivacyPolicyCount())
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_privacy_policy)
).also {
it.performClick()
}
assertEquals(1, testSetup.getPrivacyPolicyCount())
}
@Test
@MediumTest
fun on_about_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
assertEquals(0, testSetup.getAboutCount())
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_about)
).also {
it.performClick()
}
assertEquals(1, testSetup.getAboutCount())
}
@Test
@SmallTest
fun troubleshooting_menu_visible_test() {
SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new(isEnabled = true))
composeTestRule.onNodeWithTag(SettingsTag.TROUBLESHOOTING_MENU).also {
it.assertExists()
}
}
@Test
@SmallTest
fun troubleshooting_menu_not_visible_test() {
SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new(isEnabled = false))
composeTestRule.onNodeWithTag(SettingsTag.TROUBLESHOOTING_MENU).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun troubleshooting_rescan_test() {
val testSetup = SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isRescanEnabled = true
)
) {
assertEquals(0, testSetup.getRescanCount())
)
composeTestRule.onNodeWithContentDescription(
getStringResource(R.string.settings_overflow_content_description)
).also {
it.performClick()
}
assertEquals(0, testSetup.getRescanCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also {
it.performClick()
}
composeTestRule.openTroubleshootingMenu()
assertEquals(1, testSetup.getRescanCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_troubleshooting_rescan)).also {
it.performClick()
}
assertEquals(1, testSetup.getRescanCount())
}
@Test
@MediumTest
fun toggle_background_sync() = runTest {
val testSetup = TestSetup(composeTestRule)
fun troubleshooting_background_sync_test() {
val testSetup = SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isBackgroundSyncEnabled = true
)
)
assertEquals(0, testSetup.getBackgroundSyncToggleCount())
assertEquals(0, testSetup.getBackgroundSyncCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_enable_background_sync)).also {
composeTestRule.openTroubleshootingMenu()
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_troubleshooting_enable_background_sync)
).also {
it.performClick()
}
assertEquals(1, testSetup.getBackgroundSyncToggleCount())
assertEquals(1, testSetup.getBackgroundSyncCount())
}
@Test
@MediumTest
fun toggle_keep_screen_on() = runTest {
val testSetup = TestSetup(composeTestRule)
fun troubleshooting_keep_screen_on_during_sync_test() {
val testSetup = SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isKeepScreenOnDuringSyncEnabled = true
)
)
assertEquals(0, testSetup.getKeepScreenOnSyncToggleCount())
assertEquals(0, testSetup.getKeepScreenOnSyncCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_enable_keep_screen_on)).also {
composeTestRule.openTroubleshootingMenu()
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_troubleshooting_enable_keep_screen_on)
).also {
it.performClick()
}
assertEquals(1, testSetup.getKeepScreenOnSyncToggleCount())
assertEquals(1, testSetup.getKeepScreenOnSyncCount())
}
@Test
@MediumTest
fun toggle_analytics() = runTest {
val testSetup = TestSetup(composeTestRule)
fun troubleshooting_analytics_test() {
val testSetup = SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isAnalyticsEnabled = true
)
)
assertEquals(0, testSetup.getAnalyticsToggleCount())
assertEquals(0, testSetup.getAnalyticsCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_enable_analytics)).also {
composeTestRule.openTroubleshootingMenu()
composeTestRule.onNodeWithText(
getStringResource(R.string.settings_troubleshooting_enable_analytics)
).also {
it.performClick()
}
assertEquals(1, testSetup.getAnalyticsToggleCount())
}
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
private val onBackCount = AtomicInteger(0)
private val onBackupCount = AtomicInteger(0)
private val onRescanCount = AtomicInteger(0)
private val onBackgroundSyncChangedCount = AtomicInteger(0)
private val onKeepScreenOnChangedCount = AtomicInteger(0)
private val onAnalyticsChangedCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getBackupCount(): Int {
composeTestRule.waitForIdle()
return onBackupCount.get()
}
fun getRescanCount(): Int {
composeTestRule.waitForIdle()
return onRescanCount.get()
}
fun getBackgroundSyncToggleCount(): Int {
composeTestRule.waitForIdle()
return onBackgroundSyncChangedCount.get()
}
fun getKeepScreenOnSyncToggleCount(): Int {
composeTestRule.waitForIdle()
return onKeepScreenOnChangedCount.get()
}
fun getAnalyticsToggleCount(): Int {
composeTestRule.waitForIdle()
return onAnalyticsChangedCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
Settings(
isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true,
isRescanEnabled = true,
onBack = {
onBackCount.incrementAndGet()
},
onRescanWallet = {
onRescanCount.incrementAndGet()
},
onBackgroundSyncSettingsChanged = {
onBackgroundSyncChangedCount.incrementAndGet()
},
onIsKeepScreenOnDuringSyncSettingsChanged = {
onKeepScreenOnChangedCount.incrementAndGet()
},
onAnalyticsSettingsChanged = {
onAnalyticsChangedCount.incrementAndGet()
}
)
}
}
}
assertEquals(1, testSetup.getAnalyticsCount())
}
}
fun ComposeContentTestRule.openTroubleshootingMenu() {
onNodeWithContentDescription(
getStringResource(R.string.settings_troubleshooting_menu_content_description)
).also {
it.performClick()
}
}

View File

@ -6,10 +6,12 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.screen.settings.view.Settings
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
@ -45,18 +47,26 @@ private fun WrapSettings(
// Display loading indicator
} else {
Settings(
isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing,
isAnalyticsEnabled = isAnalyticsEnabled,
isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current),
TroubleshootingParameters(
isEnabled = BuildConfig.DEBUG,
isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing,
isAnalyticsEnabled = isAnalyticsEnabled,
isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current),
),
onBack = goBack,
onBackup = {},
onDocumentation = {},
onPrivacyPolicy = {},
onFeedback = {},
onAbout = {},
onRescanWallet = {
walletViewModel.rescanBlockchain()
},
onBackgroundSyncSettingsChanged = {
settingsViewModel.setBackgroundSyncEnabled(it)
},
onIsKeepScreenOnDuringSyncSettingsChanged = {
onKeepScreenOnDuringSyncSettingsChanged = {
settingsViewModel.setKeepScreenOnWhileSyncing(it)
},
onAnalyticsSettingsChanged = {

View File

@ -0,0 +1,8 @@
package co.electriccoin.zcash.ui.screen.settings
/**
* These are only used for automated testing.
*/
object SettingsTag {
const val TROUBLESHOOTING_MENU = "troubleshooting_menu"
}

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.screen.settings.model
data class TroubleshootingParameters(
val isEnabled: Boolean,
val isBackgroundSyncEnabled: Boolean,
val isKeepScreenOnDuringSyncEnabled: Boolean,
val isAnalyticsEnabled: Boolean,
val isRescanEnabled: Boolean,
)

View File

@ -1,7 +1,12 @@
package co.electriccoin.zcash.ui.screen.settings.view
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@ -9,6 +14,9 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@ -16,35 +24,47 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.SwitchWithLabel
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
import co.electriccoin.zcash.ui.screen.settings.SettingsTag
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
@Preview("Settings")
@Composable
private fun PreviewSettings() {
ZcashTheme(darkTheme = true) {
ZcashTheme(darkTheme = false) {
GradientSurface {
Settings(
isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true,
isRescanEnabled = true,
TroubleshootingParameters(
isEnabled = false,
isBackgroundSyncEnabled = false,
isKeepScreenOnDuringSyncEnabled = false,
isAnalyticsEnabled = false,
isRescanEnabled = false
),
onBack = {},
onBackup = {},
onDocumentation = {},
onPrivacyPolicy = {},
onFeedback = {},
onAbout = {},
onRescanWallet = {},
onBackgroundSyncSettingsChanged = {},
onIsKeepScreenOnDuringSyncSettingsChanged = {},
onKeepScreenOnDuringSyncSettingsChanged = {},
onAnalyticsSettingsChanged = {}
)
}
@ -54,81 +74,134 @@ private fun PreviewSettings() {
@Composable
@Suppress("LongParameterList")
fun Settings(
isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean,
isRescanEnabled: Boolean,
troubleshootingParameters: TroubleshootingParameters,
onBack: () -> Unit,
onBackup: () -> Unit,
onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit,
onAbout: () -> Unit,
onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit
) {
Scaffold(topBar = {
SettingsTopAppBar(
isRescanEnabled = isRescanEnabled,
troubleshootingParameters = troubleshootingParameters,
onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged,
onKeepScreenOnDuringSyncSettingsChanged = onKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged,
onRescanWallet = onRescanWallet,
onBack = onBack,
onRescanWallet = onRescanWallet
)
}) { paddingValues ->
SettingsMainContent(
isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnDuringSyncEnabled,
isAnalyticsEnabled = isAnalyticsEnabled,
onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged,
onIsKeepScreenOnDuringSyncSettingsChanged = onIsKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged,
modifier = Modifier
.verticalScroll(
rememberScrollState()
)
.padding(
top = paddingValues.calculateTopPadding() + dimens.spacingDefault,
bottom = paddingValues.calculateTopPadding() + dimens.spacingDefault,
start = dimens.spacingDefault,
end = dimens.spacingDefault
)
top = paddingValues.calculateTopPadding() + dimens.spacingHuge,
bottom = paddingValues.calculateBottomPadding() + dimens.spacingHuge,
start = dimens.spacingHuge,
end = dimens.spacingHuge
),
onBackup = onBackup,
onDocumentation = onDocumentation,
onPrivacyPolicy = onPrivacyPolicy,
onFeedback = onFeedback,
onAbout = onAbout,
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongParameterList")
private fun SettingsTopAppBar(
isRescanEnabled: Boolean,
troubleshootingParameters: TroubleshootingParameters,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit,
onRescanWallet: () -> Unit,
onBack: () -> Unit,
onRescanWallet: () -> Unit
) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.settings_header)) },
CenterAlignedTopAppBar(
title = {
Text(
text = stringResource(id = R.string.settings_header).uppercase(),
style = ZcashTheme.typography.primary.titleSmall,
color = ZcashTheme.colors.screenTitleColor
)
},
navigationIcon = {
IconButton(
onClick = onBack
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.settings_back_content_description)
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.settings_back_content_description)
)
}
Text(
text = stringResource(id = R.string.settings_back).uppercase(),
style = ZcashTheme.typography.primary.bodyMedium
)
}
},
actions = {
if (isRescanEnabled) {
TroubleshootingMenu(onRescanWallet)
if (troubleshootingParameters.isEnabled) {
TroubleshootingMenu(
troubleshootingParameters,
onBackgroundSyncSettingsChanged,
onKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged,
onRescanWallet
)
}
}
)
}
/**
* Add icon to Troubleshooting menu. No content description, as this is debug only menu.
*/
@Composable
private fun AddIcon(enabled: Boolean) {
if (enabled) {
Icon(
imageVector = Icons.Outlined.CheckCircle,
contentDescription = null
)
} else {
Icon(
imageVector = Icons.Outlined.Cancel,
contentDescription = null
)
}
}
@Composable
private fun TroubleshootingMenu(
troubleshootParams: TroubleshootingParameters,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit,
onRescanWallet: () -> Unit
) {
Column {
Column(
modifier = Modifier.testTag(SettingsTag.TROUBLESHOOTING_MENU)
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.settings_overflow_content_description)
contentDescription = stringResource(id = R.string.settings_troubleshooting_menu_content_description)
)
}
@ -137,12 +210,41 @@ private fun TroubleshootingMenu(
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_rescan)) },
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_background_sync)) },
onClick = {
onRescanWallet()
onBackgroundSyncSettingsChanged(!troubleshootParams.isBackgroundSyncEnabled)
expanded = false
}
},
leadingIcon = { AddIcon(troubleshootParams.isBackgroundSyncEnabled) }
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_keep_screen_on)) },
onClick = {
onKeepScreenOnDuringSyncSettingsChanged(!troubleshootParams.isKeepScreenOnDuringSyncEnabled)
expanded = false
},
leadingIcon = { AddIcon(troubleshootParams.isKeepScreenOnDuringSyncEnabled) }
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_analytics)) },
onClick = {
onAnalyticsSettingsChanged(!troubleshootParams.isAnalyticsEnabled)
expanded = false
},
leadingIcon = { AddIcon(troubleshootParams.isAnalyticsEnabled) }
)
// isRescanEnabled means if this feature should be visible, not whether it is enabled as in the case of
// the previous booleans
if (troubleshootParams.isRescanEnabled) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_rescan)) },
onClick = {
onRescanWallet()
expanded = false
},
leadingIcon = { AddIcon(true) }
)
}
}
}
}
@ -150,35 +252,77 @@ private fun TroubleshootingMenu(
@Composable
@Suppress("LongParameterList")
private fun SettingsMainContent(
isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit,
onBackup: () -> Unit,
onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit,
onAbout: () -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_background_sync),
state = isBackgroundSyncEnabled,
onStateChange = { onBackgroundSyncSettingsChanged(!isBackgroundSyncEnabled) }
Column(
Modifier
.fillMaxHeight()
.fillMaxWidth()
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryButton(
onClick = onBackup,
text = stringResource(R.string.settings_backup_wallet),
outerPaddingValues = PaddingValues(
horizontal = dimens.spacingNone,
vertical = dimens.spacingSmall
),
)
Spacer(modifier = Modifier.height(dimens.spacingXlarge))
Spacer(modifier = Modifier.height(dimens.spacingDefault))
SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_keep_screen_on),
state = isKeepScreenOnDuringSyncEnabled,
onStateChange = { onIsKeepScreenOnDuringSyncSettingsChanged(!isKeepScreenOnDuringSyncEnabled) }
PrimaryButton(
onClick = onFeedback,
text = stringResource(R.string.settings_send_us_feedback),
outerPaddingValues = PaddingValues(
horizontal = dimens.spacingNone,
vertical = dimens.spacingSmall
),
)
Spacer(modifier = Modifier.height(dimens.spacingXlarge))
Spacer(modifier = Modifier.height(dimens.spacingDefault))
SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_analytics),
state = isAnalyticsEnabled,
onStateChange = { onAnalyticsSettingsChanged(!isAnalyticsEnabled) }
PrimaryButton(
onClick = onPrivacyPolicy,
text = stringResource(R.string.settings_privacy_policy),
outerPaddingValues = PaddingValues(
horizontal = dimens.spacingNone,
vertical = dimens.spacingSmall
),
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onDocumentation,
text = stringResource(R.string.settings_documentation),
outerPaddingValues = PaddingValues(
horizontal = dimens.spacingNone,
vertical = dimens.spacingSmall
),
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
PrimaryButton(
onClick = onAbout,
text = stringResource(R.string.settings_about),
outerPaddingValues = PaddingValues(
horizontal = dimens.spacingNone,
vertical = dimens.spacingSmall
),
)
}
}

View File

@ -1,11 +1,18 @@
<resources>
<string name="settings_header">Settings</string>
<string name="settings_back">Back</string>
<string name="settings_back_content_description">Back</string>
<string name="settings_overflow_content_description">Additional settings</string>
<string name="settings_rescan">Rescan blockchain</string>
<string name="settings_enable_background_sync">Background sync</string>
<string name="settings_enable_keep_screen_on">Keep screen on during sync</string>
<string name="settings_enable_analytics">Report crashes</string>
<string name="settings_troubleshooting_menu_content_description">Additional settings</string>
<string name="settings_troubleshooting_rescan">Rescan blockchain</string>
<string name="settings_troubleshooting_enable_background_sync">Background sync</string>
<string name="settings_troubleshooting_enable_keep_screen_on">Keep screen on during sync</string>
<string name="settings_troubleshooting_enable_analytics">Report crashes</string>
<string name="settings_backup_wallet">Backup wallet</string>
<string name="settings_documentation">Documentation</string>
<string name="settings_privacy_policy">Privacy policy</string>
<string name="settings_send_us_feedback">Send us feedback</string>
<string name="settings_about">About</string>
</resources>