From 7abb2d9c08b41e4419ba8a1c5d1cf1762a95c251 Mon Sep 17 00:00:00 2001 From: AmadeyKuspakov Date: Mon, 31 Jul 2023 02:46:20 +0400 Subject: [PATCH 1/3] Addition of night mode --- app/src/main/AndroidManifest.xml | 2 - .../main/java/jp/co/soramitsu/sora/SoraApp.kt | 6 + .../splash/presentation/SplashActivity.kt | 33 +++- app/src/main/res/layout/activity_splash.xml | 4 +- .../soramitsu/common/base/SoraBaseFragment.kt | 13 +- .../common/domain/DarkThemeManager.kt | 164 ++++++++++++++++++ .../compose/components/CheckboxButton.kt | 1 + .../compose/components/DetailsItem.kt | 2 + .../presentation/compose/components/Option.kt | 2 +- .../compose/components/OptionSwitch.kt | 1 + .../presentation/compose/theme/SoraTheme.kt | 5 +- .../uikit/organisms/PagerTextIndicator.kt | 11 +- .../presentation/compose/webview/WebView.kt | 11 +- .../presentation/view/SoraBaseActivity.kt | 25 ++- common/src/main/res/values-night/styles.xml | 18 +- common/src/main/res/values/styles.xml | 1 + .../assetdetails/AssetDetailsBalanceCard.kt | 3 + .../AssetDetailsTokenPriceCard.kt | 1 + .../compose/assetdetails/AssetIdCard.kt | 1 + .../compose/assetdetails/XorBalancesDialog.kt | 3 + .../presentation/screen/TxHistoryListItem.kt | 11 +- .../screen/TxHistoryListScreen.kt | 6 +- .../presentation/MainActivity.kt | 4 + .../inappupdate/FlexibleUpdateDialog.kt | 13 +- .../presentation/pincode/PincodeScreen.kt | 1 + .../profile/appsettings/AppSettingsScreen.kt | 4 +- .../appsettings/AppSettingsViewModel.kt | 28 ++- .../profile/debugmenu/DebugMenuFragment.kt | 37 +++- .../profile/debugmenu/DebugMenuScreen.kt | 3 + .../src/main/res/layout/activity_main.xml | 2 +- .../presentation/OnboardingActivity.kt | 8 +- .../presentation/OnboardingViewModel.kt | 4 +- .../backup_password/BackupPasswordScreen.kt | 1 + .../create_account/CreateAccountScreen.kt | 1 + .../enter_passphrase/EnterPassphraseScreen.kt | 1 + .../account_details/AccountDetailsFragment.kt | 2 + .../account_list/AccountWithIcon.kt | 1 + .../export_account/backup/BackupScreen.kt | 3 + .../backup/MnemonicWordsColumn.kt | 3 + .../backup/json/BackupJsonScreen.kt | 1 + .../ImportAccountPasswordScreen.kt | 2 + .../ImportAccountSuccessScreen.kt | 1 + .../MnemonicConfirmationScreen.kt | 3 + .../presentation/tutorial/TutorialScreen.kt | 15 ++ .../compose/LiquidityAddConfirmScreen.kt | 2 + .../compose/LiquidityRemoveConfirmScreen.kt | 2 + .../components/compose/SwapAmountSquare.kt | 1 + .../components/compose/SwapConfirmScreen.kt | 2 + .../components/compose/SwapMarketsScreen.kt | 2 + .../liquidityadd/LiquidityAddFragment.kt | 11 +- .../LiquidityRemoveFragment.kt | 11 +- .../pooldetails/PoolDetailsFragment.kt | 9 +- .../presentation/screens/swap/SwapFragment.kt | 9 +- .../presentation/ReferralBondUnbondXor.kt | 4 + .../presentation/ReferrerFilled.kt | 2 + .../presentation/ReferrerInput.kt | 1 + .../presentation/YourReferralsCard.kt | 6 + .../screens/ReferralWelcomePage.kt | 3 + .../get/card/GetSoraCardScreen.kt | 7 +- .../presentation/buycrypto/BuyCryptoScreen.kt | 12 +- .../presentation/claim/ClaimScreen.kt | 3 + 61 files changed, 498 insertions(+), 51 deletions(-) create mode 100644 common/src/main/java/jp/co/soramitsu/common/domain/DarkThemeManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce02f1617..1402d7d34 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,7 +26,6 @@ @@ -55,7 +54,6 @@ + android:background="?attr/baseBackground"> : Fragment() { @Inject lateinit var debounceClickHandler: DebounceClickHandler + @Inject + lateinit var darkThemeManager: DarkThemeManager + override fun onResume() { super.onResume() activity?.safeCast()?.setColor(backgroundColor()) @@ -98,7 +104,12 @@ abstract class SoraBaseFragment : Fragment() { ): View? { return ComposeView(requireContext()).apply { setContent { - SoraAppTheme { + val isDarkModeOn: State = + darkThemeManager.darkModeStatusFlow.collectAsState() + + SoraAppTheme( + darkTheme = isDarkModeOn.value + ) { val scaffoldState = rememberScaffoldState() val scrollState = rememberScrollState() val coroutineScope = rememberCoroutineScope() diff --git a/common/src/main/java/jp/co/soramitsu/common/domain/DarkThemeManager.kt b/common/src/main/java/jp/co/soramitsu/common/domain/DarkThemeManager.kt new file mode 100644 index 000000000..c7c9ba92a --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/domain/DarkThemeManager.kt @@ -0,0 +1,164 @@ +package jp.co.soramitsu.common.domain + +import android.content.Context +import android.content.res.Configuration +import androidx.appcompat.app.AppCompatDelegate +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton +import jp.co.soramitsu.common.data.SoraPreferences +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +@Singleton +@OptIn(ExperimentalCoroutinesApi::class) +class DarkThemeManager @Inject constructor( + @ApplicationContext private val context: Context, + private val coroutineManager: CoroutineManager, + private val soraPreferences: SoraPreferences, +) { + + private data class DarkModeSettings( + val isSystemDrivenUiEnabled: Boolean, + val isDarkModeEnabled: Boolean + ) + + private val mutableDarkThemeSharedFlow = MutableSharedFlow( + replay = 1, + extraBufferCapacity = 0, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + /** + * [darkModeStatusFlow] is used to read saved state of dark mode preferences off of SoraPreferences + * and align these results with Application Configuration + * + * Hot StateFlow is used to share the same state of UI mode between screens, and avoid unnecessary + * theme switches + */ + val darkModeStatusFlow: StateFlow = mutableDarkThemeSharedFlow + .mapLatest { darkModeSettings -> + with(darkModeSettings) { + return@mapLatest if (isSystemDrivenUiEnabled) { + /* + Resources.Configuration.uiMode as opposed to AppCompatDelegate.getDefaultNightMode() + is set to Configuration.UI_MODE_NIGHT_YES when system dark mode is enabled + (e.g. extreme battery settings on, or system dark mode is enabled) + */ + val currentAppUiMode = context.resources.configuration.uiMode + .and(Configuration.UI_MODE_NIGHT_MASK) + + if (currentAppUiMode == Configuration.UI_MODE_NIGHT_YES) + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + else + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + + AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES + } else { + /* + On the other hand, AppCompatDelegate controls dark theme in scope of + our application + */ + val currentAppUiMode = AppCompatDelegate.getDefaultNightMode() + + when { + currentAppUiMode == AppCompatDelegate.MODE_NIGHT_YES && isDarkModeEnabled -> { + /* + Saved state matches system configuration; + no need to change anything + */ + true + } + currentAppUiMode == AppCompatDelegate.MODE_NIGHT_NO && !isDarkModeEnabled -> { + /* + Saved state matches system configuration; + no need to change anything + */ + false + } + else -> { + /* + Something went wrong, saved state is different from system config; + might happen due to start of the whole application (vs. moving between screens), + while user system is set to light mode + */ + + val newUiMode = if (isDarkModeEnabled) + AppCompatDelegate.MODE_NIGHT_YES else + AppCompatDelegate.MODE_NIGHT_NO + + AppCompatDelegate.setDefaultNightMode(newUiMode) + + AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES + } + } + } + } + }.flowOn(coroutineManager.main.immediate).stateIn( + scope = coroutineManager.applicationScope, + started = SharingStarted.Eagerly, // Eager mode is used to apply changes as soon as possible + initialValue = AppCompatDelegate.getDefaultNightMode() + .equals(AppCompatDelegate.MODE_NIGHT_YES) + ) + + fun updateUiModeFromCache() { + coroutineManager.applicationScope.launch { + mutableDarkThemeSharedFlow.emit( + value = DarkModeSettings( + isSystemDrivenUiEnabled = soraPreferences.getBoolean( + field = KEY_SYSTEM_DRIVEN_UI_ENABLED, + defaultValue = false + ), + isDarkModeEnabled = soraPreferences.getBoolean( + field = KEY_DARK_THEME_ENABLED, + defaultValue = false + ) + ) + ) + } + } + + suspend fun setDarkThemeEnabled(isEnabled: Boolean) { + val newDarkModeValue = soraPreferences.run { + putBoolean(KEY_SYSTEM_DRIVEN_UI_ENABLED, false) // deactivate this, if the other is chosen + putBoolean(KEY_DARK_THEME_ENABLED, isEnabled) + getBoolean(KEY_DARK_THEME_ENABLED, false) // double check, in case it didn't get saved + } + + // update listeners with new value + mutableDarkThemeSharedFlow.emit( + value = DarkModeSettings( + isSystemDrivenUiEnabled = false, + isDarkModeEnabled = newDarkModeValue + ) + ) + } + + suspend fun setSystemDrivenUiEnabled(isEnabled: Boolean) { + val newDarkModeValue = soraPreferences.run { + putBoolean(KEY_DARK_THEME_ENABLED, false) // deactivate this, if the other is chosen + putBoolean(KEY_SYSTEM_DRIVEN_UI_ENABLED, isEnabled) + getBoolean(KEY_SYSTEM_DRIVEN_UI_ENABLED, false) // double check, in case it didn't get saved + } + + // update listeners with new value + mutableDarkThemeSharedFlow.emit( + value = DarkModeSettings( + isSystemDrivenUiEnabled = newDarkModeValue, + isDarkModeEnabled = false + ) + ) + } + + private companion object { + const val KEY_DARK_THEME_ENABLED = "KEY_DARK_THEME_ENABLED" + const val KEY_SYSTEM_DRIVEN_UI_ENABLED = "KEY_SYSTEM_DRIVEN_UI_ENABLED" + } +} diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/CheckboxButton.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/CheckboxButton.kt index a7277dfa8..4c53f6238 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/CheckboxButton.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/CheckboxButton.kt @@ -88,6 +88,7 @@ fun CheckboxButton( .weight(1f) .padding(start = Dimens.x2), style = MaterialTheme.customTypography.paragraphS, + color = MaterialTheme.customColors.fgPrimary, text = text ) } diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/DetailsItem.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/DetailsItem.kt index 9dd1a242e..ce8e322aa 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/DetailsItem.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/DetailsItem.kt @@ -96,12 +96,14 @@ fun DetailsItem( title = { Text( text = text, + color = MaterialTheme.customColors.fgPrimary, style = MaterialTheme.customTypography.textSBold ) }, text = { Text( text = hint, + color = MaterialTheme.customColors.fgPrimary, style = MaterialTheme.customTypography.paragraphSBold ) }, diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/Option.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/Option.kt index 24e9a54ab..7a307c04a 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/Option.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/Option.kt @@ -69,7 +69,7 @@ fun Option( bottomDivider: Boolean, @DrawableRes iconEnd: Int = R.drawable.ic_chevron_right_24, tint: Boolean = true, - textColor: Color = Color.Unspecified, + textColor: Color = MaterialTheme.customColors.fgPrimary, enabled: Boolean = true, onClick: () -> Unit ) { diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/OptionSwitch.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/OptionSwitch.kt index 414b8425e..687f0e54a 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/OptionSwitch.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/components/OptionSwitch.kt @@ -99,6 +99,7 @@ fun OptionSwitch( ) { Text( text = label, + color = MaterialTheme.customColors.fgPrimary, style = MaterialTheme.customTypography.textM, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/theme/SoraTheme.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/theme/SoraTheme.kt index f56d9b62e..2e3b08e2c 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/theme/SoraTheme.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/theme/SoraTheme.kt @@ -69,7 +69,7 @@ private val LightColors = androidx.compose.material.lightColors( onBackground = ThemeColors.Background ) -private val DarkColors = androidx.compose.material.lightColors( +private val DarkColors = androidx.compose.material.darkColors( primary = ThemeColorsDark.Primary, onPrimary = ThemeColorsDark.OnPrimary, secondary = ThemeColorsDark.Secondary, @@ -85,9 +85,8 @@ fun SoraAppTheme( ) { AppTheme( darkTheme = darkTheme, - // todo fix lightColors = soraLightColors, - darkColors = soraLightColors, + darkColors = soraDarkColors, typography = soraTypography, borderRadius = soraBorderRadius, content = content diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/uikit/organisms/PagerTextIndicator.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/uikit/organisms/PagerTextIndicator.kt index c0b7ebe50..f19184761 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/uikit/organisms/PagerTextIndicator.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/uikit/organisms/PagerTextIndicator.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.boundsInParent import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalLayoutDirection @@ -63,6 +62,7 @@ import jp.co.soramitsu.common.presentation.compose.extension.noRippleClickable import jp.co.soramitsu.common.presentation.compose.uikit.tokens.Text import jp.co.soramitsu.common.presentation.compose.uikit.tokens.retrieveString import jp.co.soramitsu.ui_core.resources.Dimens +import jp.co.soramitsu.ui_core.theme.customColors import jp.co.soramitsu.ui_core.theme.customTypography import kotlin.math.abs @@ -80,6 +80,11 @@ fun PagerTextIndicator( val layoutDirection = LocalLayoutDirection.current + val surfaceColor = MaterialTheme.customColors.bgSurfaceInverted + + val selectedItemTextColor = MaterialTheme.customColors.fgInverted + val notSelectedItemTextColor = MaterialTheme.customColors.fgPrimary + Row( modifier = modifier .drawWithCache { @@ -118,7 +123,7 @@ fun PagerTextIndicator( val sectorHeight = size.height drawRoundRect( - color = Color.Black, + color = surfaceColor, topLeft = Offset(sectorOffsetXLerp, sectorOffsetY), size = Size(sectorWidthLerp, sectorHeight), cornerRadius = cornerRadius @@ -142,7 +147,7 @@ fun PagerTextIndicator( text = indicatorsArray[index].retrieveString(), style = MaterialTheme.customTypography.textSBold, color = if (index == currentPageRetriever()) - Color.White else Color.Black, + selectedItemTextColor else notSelectedItemTextColor, ) } } diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/webview/WebView.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/webview/WebView.kt index c2d071862..b325148ce 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/compose/webview/WebView.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/compose/webview/WebView.kt @@ -34,13 +34,16 @@ package jp.co.soramitsu.common.presentation.compose.webview import android.webkit.WebView import android.webkit.WebViewClient +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import jp.co.soramitsu.common.base.ProgressDialog +import jp.co.soramitsu.ui_core.theme.customColors @Composable fun WebView( @@ -48,11 +51,15 @@ fun WebView( onPageFinished: () -> Unit ) { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .background(color = MaterialTheme.customColors.bgPage) + .fillMaxSize(), contentAlignment = Alignment.Center ) { AndroidView( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .background(color = MaterialTheme.customColors.bgSurface) + .fillMaxSize(), factory = { context -> WebView(context).apply { webViewClient = object : WebViewClient() { diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt index ae6122d3e..924f84168 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/view/SoraBaseActivity.kt @@ -50,28 +50,41 @@ import androidx.compose.material.Scaffold import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import javax.inject.Inject import jp.co.soramitsu.common.R import jp.co.soramitsu.common.base.AlertDialogData +import jp.co.soramitsu.common.domain.DarkThemeManager import jp.co.soramitsu.common.presentation.compose.components.AlertDialogContent import jp.co.soramitsu.common.presentation.compose.components.Toolbar import jp.co.soramitsu.common.presentation.compose.theme.SoraAppTheme import jp.co.soramitsu.common.presentation.viewmodel.BaseViewModel +import jp.co.soramitsu.common.util.ext.attrColor import jp.co.soramitsu.ui_core.theme.customColors abstract class SoraBaseActivity : AppCompatActivity() { abstract val viewModel: T + @Inject + lateinit var darkThemeManager: DarkThemeManager + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - SoraAppTheme { + val isDarkModeOn: State = + darkThemeManager.darkModeStatusFlow.collectAsState() + + SoraAppTheme( + darkTheme = isDarkModeOn.value + ) { val scaffoldState = rememberScaffoldState() val scrollState = rememberScrollState() val openAlertDialog = remember { mutableStateOf(AlertDialogData()) } @@ -120,7 +133,7 @@ abstract class SoraBaseActivity : AppCompatActivity() { .statusBarsPadding() .imePadding(), scaffoldState = scaffoldState, - backgroundColor = MaterialTheme.customColors.fgPrimary.copy(alpha = 0f), + backgroundColor = MaterialTheme.customColors.bgPage.copy(alpha = 0f), topBar = { Toolbar( toolbarState = viewModel.toolbarState.observeAsState().value, @@ -146,8 +159,16 @@ abstract class SoraBaseActivity : AppCompatActivity() { } } + override fun onResume() { + super.onResume() + window.statusBarColor = attrColor(backgroundColor()) + window.navigationBarColor = attrColor(backgroundColor()) + } + abstract fun onToolbarNavigation() + open fun backgroundColor() = R.attr.baseBackground + @Composable abstract fun Content(padding: PaddingValues, scrollState: ScrollState) } diff --git a/common/src/main/res/values-night/styles.xml b/common/src/main/res/values-night/styles.xml index c2153b995..8e73a84a9 100644 --- a/common/src/main/res/values-night/styles.xml +++ b/common/src/main/res/values-night/styles.xml @@ -25,7 +25,7 @@ @color/grey_700 @color/grey_500 @color/grey_400 - @color/neu_color_300 + #FF070707 @color/neu_color_white @color/backgroundDarkColor @color/grey_300 @@ -43,12 +43,12 @@ @color/neu_disabled_text_grey @color/neu_disabled_text_grey_2 - @color/grey_100 + @color/grey_900 @color/hoverSecondaryColor @color/pressedSecondaryColor @color/focusedSecondaryColor - @color/polkaswap_background_alfa + #FF070707 @color/neu_brand_polkaswap @color/neu_divider_color @color/neu_black_default @@ -70,7 +70,7 @@ @color/neu_status @color/neu_status_warning_day @color/neu_color_black - @color/neu_color_white + @color/neu_color_black @color/neu_color_red @color/neu_status_pink_warning_day @color/neu_color_grey_100 @@ -80,7 +80,7 @@ @color/neu_positive @color/neu_selected_primary @color/tint_above_background - #281818 + #FFFCFCFC #9d8181 @color/neu_disabled_on_night @@ -88,4 +88,12 @@ @drawable/rounded_button_background + + \ No newline at end of file diff --git a/common/src/main/res/values/styles.xml b/common/src/main/res/values/styles.xml index d01982639..fc8c3667f 100644 --- a/common/src/main/res/values/styles.xml +++ b/common/src/main/res/values/styles.xml @@ -93,6 +93,7 @@ true @android:color/transparent @android:color/transparent + @color/neu_color_300