diff --git a/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap index d737c15bbb0..fe77252cc5d 100644 --- a/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap @@ -220,7 +220,7 @@ exports[`CategoryModal should render with a subcategory 1`] = ` numberOfLines={1} style={ { - "color": "#1a1a1a", + "color": "#FFFFFF", "fontFamily": "Quicksand-Bold", "fontSize": 14, "marginHorizontal": 7, @@ -1836,7 +1836,7 @@ exports[`CategoryModal should render with an empty subcategory 1`] = ` numberOfLines={1} style={ { - "color": "#1a1a1a", + "color": "#FFFFFF", "fontFamily": "Quicksand-Bold", "fontSize": 14, "marginHorizontal": 7, diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap index 51f4515aa7b..bef4933e6dc 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap @@ -2,22 +2,36 @@ exports[`CreateWalletAccountSelect renders 1`] = ` [ - - , + , - , + , - , + , - , + , - , + , - , + , - , + , - , + , - @@ -174,26 +159,10 @@ exports[`SendScene2 1 spendTarget 1`] = ` } r="238.7781690140845" /> - - , + , - @@ -1346,26 +1300,10 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` } r="238.7781690140845" /> - - , + , - @@ -2706,26 +2629,10 @@ exports[`SendScene2 2 spendTargets 1`] = ` } r="238.7781690140845" /> - - , + , - @@ -4037,26 +3929,10 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` } r="238.7781690140845" /> - - , + , - @@ -5213,26 +5074,10 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` } r="238.7781690140845" /> - - , + , - @@ -6365,26 +6195,10 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` } r="238.7781690140845" /> - - , + , - @@ -7362,26 +7161,10 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` } r="238.7781690140845" /> - - , + , - @@ -8652,26 +8420,10 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` } r="238.7781690140845" /> - - , + , - @@ -9877,26 +9614,10 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` } r="238.7781690140845" /> - - , + , - @@ -11061,26 +10767,10 @@ exports[`SendScene2 Render SendScene 1`] = ` } r="238.7781690140845" /> - - , + , - , + , - , + , - @@ -174,26 +159,10 @@ exports[`TransactionDetailsScene should render 1`] = ` } r="238.7781690140845" /> - - , + , - @@ -1923,26 +1877,10 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi } r="238.7781690140845" /> - - , + , { ...DefaultTheme, colors: { ...DefaultTheme.colors, - background: theme.background.color + background: theme.backgroundGradientColors[0] } } }, [theme]) diff --git a/src/components/buttons/MinimalButton.tsx b/src/components/buttons/MinimalButton.tsx index 288350f63ab..dc61f436308 100644 --- a/src/components/buttons/MinimalButton.tsx +++ b/src/components/buttons/MinimalButton.tsx @@ -95,7 +95,7 @@ const getStyles = cacheStyles((theme: Theme) => { }, labelSelected: { ...labelCommon, - color: theme.background.color + color: theme.secondaryButtonText }, labelDisabled: { ...labelCommon, diff --git a/src/components/common/SceneWrapper.tsx b/src/components/common/SceneWrapper.tsx index 26ef4b7b6da..3e667c34327 100644 --- a/src/components/common/SceneWrapper.tsx +++ b/src/components/common/SceneWrapper.tsx @@ -8,12 +8,13 @@ import { EdgeInsets, useSafeAreaFrame, useSafeAreaInsets } from 'react-native-sa import { useSceneDrawerState } from '../../state/SceneDrawerState' import { useSelector } from '../../types/reactRedux' import { NavigationBase } from '../../types/routerTypes' +import { OverrideDots } from '../../types/Theme' import { maybeComponent } from '../hoc/maybeComponent' import { NotificationView } from '../notification/NotificationView' import { useTheme } from '../services/ThemeContext' import { MAX_TAB_BAR_HEIGHT } from '../themed/MenuTabs' import { SceneDrawer } from '../themed/SceneDrawer' -import { DotsBackground } from '../ui4/DotsBackground' +import { AccentColors, DotsBackground } from '../ui4/DotsBackground' import { KeyboardTracker } from './KeyboardTracker' export interface InsetStyles { @@ -37,12 +38,17 @@ interface SceneWrapperProps { // to changes to the info. children: React.ReactNode | ((info: SceneWrapperInfo) => React.ReactNode) - // Adjusts the blurred dots background: - accentColor?: string + // Object specifying accent colors to use for DotsBackground + accentColors?: AccentColors // True if this scene should shrink to avoid the keyboard: avoidKeyboard?: boolean + // Optional backgroundGradient overrides + backgroundGradientColors?: string[] + backgroundGradientStart?: { x: number; y: number } + backgroundGradientEnd?: { x: number; y: number } + // True if this scene has a header (with back button & such): hasHeader?: boolean @@ -55,6 +61,9 @@ interface SceneWrapperProps { // Settings for when using ScrollView keyboardShouldPersistTaps?: 'always' | 'never' | 'handled' + // Override existing background dots parameters + overrideDots?: OverrideDots + // Padding to add inside the scene border: padding?: number @@ -80,8 +89,12 @@ interface SceneWrapperProps { */ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { const { - accentColor, + overrideDots, + accentColors, avoidKeyboard = false, + backgroundGradientColors, + backgroundGradientStart, + backgroundGradientEnd, children, renderDrawer, hasHeader = true, @@ -152,7 +165,13 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { return ( - + {} @@ -64,6 +66,10 @@ interface CurrencyMinimumPopupState { [pluginId: string]: ModalState } +interface HookProps { + iconColor?: string +} + type Props = StateProps & DispatchProps & OwnProps & ThemeProps interface State { @@ -82,7 +88,7 @@ interface AddressInfo { const inputAccessoryViewID: string = 'cancelHeaderId' -export class RequestSceneComponent extends React.Component { +export class RequestSceneComponent extends React.Component { flipInputRef: React.RefObject unsubscribeAddressChanged: (() => void) | undefined @@ -286,7 +292,7 @@ export class RequestSceneComponent extends React.Component { } render() { - const { currencyCode, exchangeSecondaryToPrimaryRatio, wallet, primaryCurrencyInfo, theme } = this.props + const { currencyCode, exchangeSecondaryToPrimaryRatio, iconColor, wallet, primaryCurrencyInfo, theme } = this.props const styles = getStyles(theme) if (currencyCode == null || primaryCurrencyInfo == null || exchangeSecondaryToPrimaryRatio == null || wallet == null) { @@ -307,10 +313,28 @@ export class RequestSceneComponent extends React.Component { // Selected denomination const denomString = `1 ${primaryCurrencyInfo.displayDenomination.name}` + const accentColors: AccentColors = { + // Transparent fallback for while iconColor is loading + iconAccentColor: iconColor ?? '#00000000' + } + + const backgroundColors = [...theme.assetBackgroundGradientColors] + if (iconColor != null) { + const scaledColor = darkenHexColor(iconColor, theme.assetBackgroundColorScale) + backgroundColors[0] = scaledColor + } + return keysOnlyMode ? ( this.renderKeysOnlyMode() ) : ( - + {lstrings.fragment_request_subtitle} @@ -539,7 +563,7 @@ const getStyles = cacheStyles((theme: Theme) => ({ } })) -export const RequestScene = connect( +const RequestSceneConnected = connect( state => { const { account } = state.core const { currencyWallets } = account @@ -596,3 +620,16 @@ export const RequestScene = connect( } }) )(withTheme(RequestSceneComponent)) + +export const RequestScene = (props: OwnProps) => { + const account = useSelector(state => state.core.account) + const currencyCode = useSelector(state => state.ui.wallets.selectedCurrencyCode) + const walletId = useSelector(state => state.ui.wallets.selectedWalletId) + const wallet = account.currencyWallets[walletId] ?? {} + + const { pluginId = '' } = wallet.currencyInfo ?? {} + const tokenId = getTokenId(account, pluginId, currencyCode) + + const iconColor = useIconColor({ pluginId, tokenId: tokenId !== undefined ? tokenId : '' }) + return +} diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 9deb04a21a7..f08b68a844a 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -24,6 +24,7 @@ import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { useDisplayDenom } from '../../hooks/useDisplayDenom' import { useExchangeDenom } from '../../hooks/useExchangeDenom' import { useHandler } from '../../hooks/useHandler' +import { useIconColor } from '../../hooks/useIconColor' import { useMount } from '../../hooks/useMount' import { useUnmount } from '../../hooks/useUnmount' import { useWatch } from '../../hooks/useWatch' @@ -37,7 +38,7 @@ import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { addToFioAddressCache, checkRecordSendFee, FIO_FEE_EXCEEDS_SUPPLIED_MAXIMUM, FIO_NO_BUNDLED_ERR_CODE, recordSend } from '../../util/FioAddressUtils' import { logActivity } from '../../util/logger' -import { convertTransactionFeeToDisplayFee, DECIMAL_PRECISION, zeroString } from '../../util/utils' +import { convertTransactionFeeToDisplayFee, darkenHexColor, DECIMAL_PRECISION, zeroString } from '../../util/utils' import { getMemoError, getMemoLabel, getMemoTitle } from '../../util/validateMemos' import { SceneWrapper } from '../common/SceneWrapper' import { styled } from '../hoc/styled' @@ -59,6 +60,7 @@ import { EditableAmountTile } from '../tiles/EditableAmountTile' import { ErrorTile } from '../tiles/ErrorTile' import { AlertCardUi4 } from '../ui4/AlertCardUi4' import { CardUi4 } from '../ui4/CardUi4' +import { AccentColors } from '../ui4/DotsBackground' import { RowUi4 } from '../ui4/RowUi4' // TODO: Check contentPadding @@ -181,6 +183,7 @@ const SendComponent = (props: Props) => { const cryptoExchangeDenomination = useExchangeDenom(pluginId, currencyCode) const parentDisplayDenom = useDisplayDenom(pluginId, currencyWallets[walletId].currencyInfo.currencyCode) const parentExchangeDenom = useExchangeDenom(pluginId, currencyWallets[walletId].currencyInfo.currencyCode) + const iconColor = useIconColor({ pluginId, tokenId }) spendInfo.tokenId = tokenId @@ -994,8 +997,28 @@ const SendComponent = (props: Props) => { disableSlider = true disabledText = lstrings.spending_limits_enter_pin } + + const accentColors: AccentColors = { + // Transparent fallback for while iconColor is loading + iconAccentColor: iconColor ?? '#00000000' + } + + const backgroundColors = [...theme.assetBackgroundGradientColors] + if (iconColor != null) { + const scaledColor = darkenHexColor(iconColor, theme.assetBackgroundColorScale) + backgroundColors[0] = scaledColor + } + return ( - + {({ insetStyles }) => ( <> { wallet: EdgeCurrencyWallet @@ -40,6 +43,7 @@ const StakeOptionsSceneComponent = (props: Props) => { const account = useSelector(state => state.core.account) const pluginId = wallet?.currencyInfo.pluginId const tokenId = pluginId ? getTokenIdForced(account, pluginId, currencyCode) : null + const iconColor = useIconColor({ pluginId, tokenId }) // // Handlers @@ -76,8 +80,26 @@ const StakeOptionsSceneComponent = (props: Props) => { ) } + const accentColors: AccentColors = { + // Transparent fallback for while iconColor is loading + iconAccentColor: iconColor ?? '#00000000' + } + + const backgroundColors = [...theme.assetBackgroundGradientColors] + if (iconColor != null) { + const scaledColor = darkenHexColor(iconColor, theme.assetBackgroundColorScale) + backgroundColors[0] = scaledColor + } + return ( - + diff --git a/src/components/scenes/TransactionDetailsScene.tsx b/src/components/scenes/TransactionDetailsScene.tsx index 37bed2d36de..cab6125a56f 100644 --- a/src/components/scenes/TransactionDetailsScene.tsx +++ b/src/components/scenes/TransactionDetailsScene.tsx @@ -13,6 +13,7 @@ import { useContactThumbnail } from '../../hooks/redux/useContactThumbnail' import { displayFiatAmount } from '../../hooks/useFiatText' import { useHandler } from '../../hooks/useHandler' import { useHistoricalRate } from '../../hooks/useHistoricalRate' +import { useIconColor } from '../../hooks/useIconColor' import { useWatch } from '../../hooks/useWatch' import { toPercentString } from '../../locales/intl' import { lstrings } from '../../locales/strings' @@ -22,7 +23,7 @@ import { useSelector } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { getCurrencyCodeWithAccount } from '../../util/CurrencyInfoHelpers' import { matchJson } from '../../util/matchJson' -import { convertNativeToExchange } from '../../util/utils' +import { convertNativeToExchange, darkenHexColor } from '../../util/utils' import { getMemoTitle } from '../../util/validateMemos' import { EdgeAnim } from '../common/EdgeAnim' import { SceneWrapper } from '../common/SceneWrapper' @@ -37,6 +38,7 @@ import { EdgeText } from '../themed/EdgeText' import { AdvancedDetailsCard } from '../ui4/AdvancedDetailsCard' import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' import { CardUi4 } from '../ui4/CardUi4' +import { AccentColors } from '../ui4/DotsBackground' import { RowUi4 } from '../ui4/RowUi4' import { SwapDetailsCard } from '../ui4/SwapDetailsCard' import { TxCryptoAmountRow } from '../ui4/TxCryptoAmountRow' @@ -53,12 +55,14 @@ export interface TransactionDetailsParams { const TransactionDetailsComponent = (props: Props) => { const { navigation, route, wallet } = props const { edgeTransaction: transaction, walletId } = route.params - const { currencyCode, metadata, nativeAmount, date, txid } = transaction + const { currencyCode, metadata, nativeAmount, date, txid, tokenId } = transaction const { currencyInfo } = wallet const theme = useTheme() const account = useSelector(state => state.core.account) const styles = getStyles(theme) + const iconColor = useIconColor({ pluginId: currencyInfo.pluginId, tokenId }) + // Choose a default category based on metadata or the txAction const { direction, iconPluginId, mergedData, savedData } = getTxActionDisplayInfo(transaction, account, wallet) @@ -294,8 +298,29 @@ const TransactionDetailsComponent = (props: Props) => { const categoriesText = formatCategory(splitCategory(localMetadata.category ?? undefined)) + const accentColors: AccentColors = { + // Transparent fallback for while iconColor is loading + iconAccentColor: iconColor ?? '#00000000' + } + + const backgroundColors = [...theme.assetBackgroundGradientColors] + if (iconColor != null) { + const scaledColor = darkenHexColor(iconColor, theme.assetBackgroundColorScale) + backgroundColors[0] = scaledColor + } + return ( - + >(FlashList) @@ -60,8 +60,6 @@ function TransactionListComponent(props: Props) { const [searchText, setSearchText] = React.useState('') const [assetStatuses, setAssetStatuses] = React.useState([]) const [iconColor, setIconColor] = React.useState() - const transparentBackground = `${theme.background.color}00` - const backgroundGradientColor = iconColor == null ? transparentBackground : `${iconColor}44` // Selectors: const exchangeDenom = useSelector(state => getExchangeDenomination(state, pluginId, currencyCode)) @@ -270,16 +268,32 @@ function TransactionListComponent(props: Props) { [handleChangeText, handleDoneSearching, handleStartSearching, isSearching, searchText] ) + const accentColors: AccentColors = { + // Transparent fallback for while iconColor is loading + iconAccentColor: iconColor ?? '#00000000' + } + + const backgroundColors = [...theme.assetBackgroundGradientColors] + if (iconColor != null) { + const scaledColor = darkenHexColor(iconColor, theme.assetBackgroundColorScale) + backgroundColors[0] = scaledColor + } + return ( - + {({ insetStyles }) => ( <> - + {accentDots.map(renderGradient)} {accentDots.map(renderCircle)} - + ) } diff --git a/src/hooks/useIconColor.ts b/src/hooks/useIconColor.ts new file mode 100644 index 00000000000..e39365c9245 --- /dev/null +++ b/src/hooks/useIconColor.ts @@ -0,0 +1,40 @@ +import * as React from 'react' +import { getColors } from 'react-native-image-colors' + +import { useState } from '../types/reactHooks' +import { EdgeAsset } from '../types/types' +import { getCurrencyIconUris } from '../util/CdnUris' + +export const useIconColor = (edgeAsset: EdgeAsset): string | undefined => { + const [color, setColor] = useState(undefined) + const primaryCurrencyIconUrl = React.useMemo(() => { + const { pluginId, tokenId } = edgeAsset + if (pluginId == null) return null + + // Get Currency Icon URI + const icon = getCurrencyIconUris(pluginId, tokenId) + return icon.symbolImage + }, [edgeAsset]) + + React.useEffect(() => { + if (primaryCurrencyIconUrl == null) return + + getColors(primaryCurrencyIconUrl, { + cache: true, + key: primaryCurrencyIconUrl + }) + .then(colors => { + if (colors.platform === 'ios') { + setColor(colors.primary) + } + if (colors.platform === 'android') { + setColor(colors.vibrant) + } + }) + .catch(err => { + console.warn(err) + }) + }, [primaryCurrencyIconUrl]) + + return color +} diff --git a/src/theme/variables/edgeDark.ts b/src/theme/variables/edgeDark.ts index a0f7d693717..43f3e4feef4 100644 --- a/src/theme/variables/edgeDark.ts +++ b/src/theme/variables/edgeDark.ts @@ -132,14 +132,15 @@ export const edgeDark: Theme = { loadingIcon: palette.edgeMint, // Background - background: { + backgroundGradientColors: [palette.backgroundBlack, palette.backgroundBlack], + backgroundGradientStart: { x: 0, y: 0 }, + backgroundGradientEnd: { x: 1, y: 1 }, + backgroundDots: { blurRadius: scale(80), - color: palette.backgroundBlack, dotOpacity: 0.25, dots: [ { // Top-left: - accent: 'keep', color: palette.white, cx: '10%', cy: '10%', @@ -154,14 +155,18 @@ export const edgeDark: Theme = { }, { // Bottom-left: - accent: 'drop', color: palette.backgroundGreen, cx: '-15%', cy: '100%', r: scale(220) } - ] + ], + assetOverrideDots: [undefined, { accentColor: 'iconAccentColor' }, null] }, + assetBackgroundGradientColors: [palette.darkAqua, palette.backgroundBlack], + assetBackgroundGradientStart: { x: 0, y: 0 }, + assetBackgroundGradientEnd: { x: 0, y: 1 }, + assetBackgroundColorScale: 0.1, // Camera Overlay cameraOverlayColor: palette.black, diff --git a/src/theme/variables/edgeLight.ts b/src/theme/variables/edgeLight.ts index 44a03901cc7..ad483dd9a63 100644 --- a/src/theme/variables/edgeLight.ts +++ b/src/theme/variables/edgeLight.ts @@ -126,15 +126,22 @@ export const edgeLight: Theme = { loadingIcon: palette.edgeBlue, // Background - background: { + backgroundGradientColors: [palette.lightestGray, palette.lightestGray], + backgroundGradientStart: { x: 0, y: 0 }, + backgroundGradientEnd: { x: 1, y: 0 }, + backgroundDots: { blurRadius: scale(80), - color: palette.lightestGray, dotOpacity: 0.3, dots: [ { color: palette.backgroundGreen, cx: '75%', cy: '25%', r: scale(175) }, - { color: palette.backgroundPurple, cx: '25%', cy: '75%', r: scale(150), accent: 'keep' } - ] + { color: palette.backgroundPurple, cx: '25%', cy: '75%', r: scale(150) } + ], + assetOverrideDots: [undefined, { accentColor: 'iconAccentColor' }, null] }, + assetBackgroundGradientColors: [palette.lightestGray, palette.lightestGray], + assetBackgroundGradientStart: { x: 0, y: 0 }, + assetBackgroundGradientEnd: { x: 0, y: 1 }, + assetBackgroundColorScale: 0.3, // Camera Overlay cameraOverlayColor: palette.gray, diff --git a/src/theme/variables/testDark.ts b/src/theme/variables/testDark.ts index fc5c69cedcd..132f3fc9226 100644 --- a/src/theme/variables/testDark.ts +++ b/src/theme/variables/testDark.ts @@ -132,14 +132,15 @@ export const testDark: Theme = { loadingIcon: palette.edgeMint, // Background - background: { + backgroundGradientColors: [palette.black, palette.black], + backgroundGradientStart: { x: 0, y: 0 }, + backgroundGradientEnd: { x: 1, y: 0 }, + backgroundDots: { blurRadius: scale(80), - color: palette.backgroundBlack, dotOpacity: 0.25, dots: [ { // Top-left: - accent: 'keep', color: palette.white, cx: '10%', cy: '10%', @@ -154,14 +155,18 @@ export const testDark: Theme = { }, { // Bottom-left: - accent: 'drop', color: palette.backgroundGreen, cx: '-15%', cy: '100%', r: scale(220) } - ] + ], + assetOverrideDots: [undefined, { accentColor: 'iconAccentColor' }, null] }, + assetBackgroundGradientColors: [palette.darkAqua, palette.black], + assetBackgroundGradientStart: { x: 0, y: 0 }, + assetBackgroundGradientEnd: { x: 0, y: 1 }, + assetBackgroundColorScale: 0.3, // Camera Overlay cameraOverlayColor: palette.black, diff --git a/src/theme/variables/testLight.ts b/src/theme/variables/testLight.ts index 79c4159916f..73b904c81a9 100644 --- a/src/theme/variables/testLight.ts +++ b/src/theme/variables/testLight.ts @@ -126,15 +126,22 @@ export const testLight: Theme = { loadingIcon: palette.edgeBlue, // Background - background: { + backgroundGradientColors: [palette.lightestGray, palette.lightestGray], + backgroundGradientStart: { x: 0, y: 0 }, + backgroundGradientEnd: { x: 1, y: 0 }, + backgroundDots: { blurRadius: scale(80), - color: palette.lightestGray, dotOpacity: 0.3, dots: [ { color: palette.backgroundGreen, cx: '75%', cy: '25%', r: scale(175) }, { color: palette.backgroundPurple, cx: '25%', cy: '75%', r: scale(150) } - ] + ], + assetOverrideDots: [undefined, { accentColor: 'iconAccentColor' }, null] }, + assetBackgroundGradientColors: [palette.lightestGray, palette.lightestGray], + assetBackgroundGradientStart: { x: 0, y: 0 }, + assetBackgroundGradientEnd: { x: 0, y: 1 }, + assetBackgroundColorScale: 0.3, // Camera Overlay cameraOverlayColor: palette.gray, diff --git a/src/types/Theme.ts b/src/types/Theme.ts index 96dc5320e2a..f930eeeaa03 100644 --- a/src/types/Theme.ts +++ b/src/types/Theme.ts @@ -1,18 +1,20 @@ import { asNumber, asObject } from 'cleaners' +import { AccentColors } from '../components/ui4/DotsBackground' + export type ImageProp = { uri: string } | number export interface ThemeDot { - // The accent color will override the dot color when it is present. - // To change this behavior, use 'keep' to preserve the dot color or - // 'drop' to remove the dot completely: - accent?: 'keep' | 'drop' + accentColor?: keyof AccentColors color: string cx: number | string cy: number | string r: number } +// Updates to dots. undefined keeps the dots, null deletes them +export type OverrideDots = Array | undefined | null> + interface ThemeGradientParams { colors: string[] start: GradientCoords @@ -96,12 +98,19 @@ export interface Theme { loadingIcon: string // Background - background: { + backgroundGradientColors: string[] + backgroundGradientStart: { x: number; y: number } + backgroundGradientEnd: { x: number; y: number } + backgroundDots: { blurRadius: number - color: string // Never include an alpha here dotOpacity: number dots: ThemeDot[] + assetOverrideDots: OverrideDots } + assetBackgroundGradientColors: string[] + assetBackgroundGradientStart: { x: number; y: number } + assetBackgroundGradientEnd: { x: number; y: number } + assetBackgroundColorScale: number // Camera Overlay cameraOverlayColor: string diff --git a/src/util/utils.ts b/src/util/utils.ts index 714a0b93cbf..b4160e89077 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -595,3 +595,40 @@ export const base58ToUuid = (base58String: string): string => { const uuid = v4({ random: bytes }) return uuid } + +/** + * Darken a color by a scale factor. + * @param hexColor of format '#1f1f1f1f' + * @param scaleFactor 0-1 with 0 being black, 1 is unchanged + * @returns darkened hex color string + */ +export const darkenHexColor = (hexColor: string, scaleFactor: number): string => { + if (scaleFactor < 0 || scaleFactor > 1) throw new Error('scaleFactor must be between 0-1') + hexColor = hexColor.replace('#', '') + + // Check for short and long hexadecimal color codes + if (hexColor.length === 3) { + // Expand short color code (e.g., #abc to #aabbcc) + hexColor = hexColor + .split('') + .map(char => char + char) + .join('') + } else if (hexColor.length !== 6) { + throw new Error('Invalid hexadecimal color code') + } + + // Parse the hexadecimal values + const r = parseInt(hexColor.slice(0, 2), 16) + const g = parseInt(hexColor.slice(2, 4), 16) + const b = parseInt(hexColor.slice(4, 6), 16) + + // Multiply each color component by the scale factor + const scaledR = Math.round(r * scaleFactor) + const scaledG = Math.round(g * scaleFactor) + const scaledB = Math.round(b * scaleFactor) + + // Convert the scaled values back to hexadecimal + const scaledHexColor = `#${scaledR.toString(16).padStart(2, '0')}${scaledG.toString(16).padStart(2, '0')}${scaledB.toString(16).padStart(2, '0')}` + + return scaledHexColor +}