feat: mobile app package (#1075)
This commit is contained in:
103
packages/mobile/src/Style/CssParser.ts
Normal file
103
packages/mobile/src/Style/CssParser.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
const PREFIX_GENERIC = '--'
|
||||
const PREFIX_STANDARD_NOTES = '--sn-stylekit'
|
||||
const PREFIX_STANDARD_NOTES_BURN = '--sn-'
|
||||
|
||||
function camelCaseToDashed(camel: string) {
|
||||
return camel.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
||||
}
|
||||
|
||||
export function objectToCss(object: any) {
|
||||
let result = ''
|
||||
for (const key of Object.keys(object)) {
|
||||
const dashed = `sn-${camelCaseToDashed(key).toLowerCase()}`
|
||||
const line = `--${dashed}: ${object[key]};\n`
|
||||
result += line
|
||||
}
|
||||
return `
|
||||
:root {
|
||||
${result}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export default class CSSParser {
|
||||
/**
|
||||
* @param css: CSS file contents in string format
|
||||
*/
|
||||
static cssToObject(css: string) {
|
||||
const object: Record<string, string> = {}
|
||||
const lines = css.split('\n')
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
if (line.startsWith(PREFIX_GENERIC)) {
|
||||
// Remove initial '--'
|
||||
if (line.startsWith(PREFIX_STANDARD_NOTES)) {
|
||||
line = line.slice(PREFIX_STANDARD_NOTES_BURN.length, line.length)
|
||||
} else {
|
||||
// Not all vars start with --sn-stylekit. e.g --background-color
|
||||
line = line.slice(PREFIX_GENERIC.length, line.length)
|
||||
}
|
||||
const parts = line.split(':')
|
||||
let key = parts[0].trim()
|
||||
let value = parts[1].trim()
|
||||
|
||||
key = this.hyphenatedStringToCamelCase(key)
|
||||
value = value.replace(';', '').trim()
|
||||
|
||||
object[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
this.resolveVariablesThatReferenceOtherVariables(object)
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
static resolveVariablesThatReferenceOtherVariables(object: Record<string, any>, round = 0) {
|
||||
for (const key of Object.keys(object)) {
|
||||
const value = object[key]
|
||||
const stripValue = 'var('
|
||||
if (typeof value === 'string' && value.startsWith(stripValue)) {
|
||||
const from = stripValue.length
|
||||
const to = value.indexOf(')')
|
||||
let varName = value.slice(from, to)
|
||||
if (varName.startsWith(PREFIX_STANDARD_NOTES)) {
|
||||
varName = varName.slice(PREFIX_STANDARD_NOTES_BURN.length, varName.length)
|
||||
} else {
|
||||
// Not all vars start with --sn-stylekit. e.g --background-color
|
||||
varName = varName.slice(PREFIX_GENERIC.length, varName.length)
|
||||
}
|
||||
varName = this.hyphenatedStringToCamelCase(varName)
|
||||
object[key] = object[varName]
|
||||
}
|
||||
}
|
||||
|
||||
// Two rounds are required. The first will replace all right hand side variables with
|
||||
// the left hand counterparts. In the first round, variables on rhs mentioned before
|
||||
// its value has been gotten to in the loop will be missed. The second round takes care of this
|
||||
// and makes sure that all values will resolve.
|
||||
if (round === 0) {
|
||||
this.resolveVariablesThatReferenceOtherVariables(object, ++round)
|
||||
}
|
||||
}
|
||||
|
||||
static hyphenatedStringToCamelCase(string: string) {
|
||||
const comps = string.split('-')
|
||||
let result = ''
|
||||
for (let i = 0; i < comps.length; i++) {
|
||||
const part = comps[i]
|
||||
if (i === 0) {
|
||||
result += part
|
||||
} else {
|
||||
result += this.capitalizeFirstLetter(part)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
}
|
||||
83
packages/mobile/src/Style/CustomActionSheet.ts
Normal file
83
packages/mobile/src/Style/CustomActionSheet.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ActionSheetOptions, useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import React, { useContext } from 'react'
|
||||
import { findNodeHandle } from 'react-native'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
export type CustomActionSheetOption =
|
||||
| {
|
||||
text: string
|
||||
key?: string
|
||||
callback?: () => void
|
||||
destructive?: boolean
|
||||
}
|
||||
| {
|
||||
text: string
|
||||
key?: string
|
||||
callback?: (option: CustomActionSheetOption) => void
|
||||
destructive?: boolean
|
||||
}
|
||||
|
||||
type TShowActionSheetParams = {
|
||||
title: string
|
||||
options: CustomActionSheetOption[]
|
||||
onCancel?: () => void
|
||||
anchor?: React.Component<any, any>
|
||||
styles?: Partial<ActionSheetOptions>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
export const createActionSheetOptions = () => {}
|
||||
|
||||
export const useCustomActionSheet = () => {
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const showActionSheet = ({
|
||||
title,
|
||||
options,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onCancel = () => {},
|
||||
anchor,
|
||||
styles = {},
|
||||
}: TShowActionSheetParams) => {
|
||||
const cancelOption: CustomActionSheetOption[] = [
|
||||
{
|
||||
text: 'Cancel',
|
||||
callback: onCancel,
|
||||
key: 'CancelItem',
|
||||
destructive: false,
|
||||
},
|
||||
]
|
||||
const tempOptions = options.concat(cancelOption)
|
||||
const destructiveIndex = tempOptions.findIndex(item => item.destructive)
|
||||
const cancelIndex = tempOptions.length - 1
|
||||
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options: tempOptions.map(option => option.text),
|
||||
destructiveButtonIndex: destructiveIndex,
|
||||
cancelButtonIndex: cancelIndex,
|
||||
title,
|
||||
containerStyle: {
|
||||
backgroundColor: theme.stylekitBorderColor,
|
||||
...styles?.containerStyle,
|
||||
},
|
||||
textStyle: {
|
||||
color: theme.stylekitForegroundColor,
|
||||
...styles.textStyle,
|
||||
},
|
||||
titleTextStyle: {
|
||||
color: theme.stylekitForegroundColor,
|
||||
...styles.titleTextStyle,
|
||||
},
|
||||
anchor: anchor ? findNodeHandle(anchor) ?? undefined : undefined,
|
||||
},
|
||||
buttonIndex => {
|
||||
const option = tempOptions[buttonIndex!]
|
||||
option.callback && option.callback(option)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return { showActionSheet }
|
||||
}
|
||||
20
packages/mobile/src/Style/Icons.ts
Normal file
20
packages/mobile/src/Style/Icons.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const ICON_ADD = 'add'
|
||||
export const ICON_CHECKMARK = 'checkmark'
|
||||
export const ICON_CLOSE = 'close'
|
||||
export const ICON_MENU = 'menu'
|
||||
export const ICON_SETTINGS = 'settings-sharp'
|
||||
export const ICON_LOCK = 'lock-closed'
|
||||
export const ICON_ALERT = 'alert-circle'
|
||||
export const ICON_BRUSH = 'brush'
|
||||
export const ICON_MEDICAL = 'medical'
|
||||
export const ICON_PRICE_TAG = 'pricetag'
|
||||
export const ICON_BOOKMARK = 'bookmark'
|
||||
export const ICON_ARCHIVE = 'archive'
|
||||
export const ICON_FINGER_PRINT = 'finger-print'
|
||||
export const ICON_SHARE = 'share'
|
||||
export const ICON_TRASH = 'trash'
|
||||
export const ICON_USER = 'person-circle'
|
||||
export const ICON_FORWARD = 'arrow-forward'
|
||||
export const ICON_HISTORY = 'time'
|
||||
export const ELIPSIS = 'ellipsis-vertical'
|
||||
export const ICON_ATTACH = 'attach'
|
||||
BIN
packages/mobile/src/Style/Images/sn-logo.png
Normal file
BIN
packages/mobile/src/Style/Images/sn-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
packages/mobile/src/Style/Images/sn-splash-logo.png
Normal file
BIN
packages/mobile/src/Style/Images/sn-splash-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
46
packages/mobile/src/Style/MobileTheme.ts
Normal file
46
packages/mobile/src/Style/MobileTheme.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ComponentContent,
|
||||
ContentType,
|
||||
DecryptedPayload,
|
||||
FillItemContent,
|
||||
SNTheme,
|
||||
ThemeDockIcon,
|
||||
} from '@standardnotes/snjs'
|
||||
import { MobileThemeVariables } from './Themes/styled-components'
|
||||
|
||||
export interface MobileThemeContent extends ComponentContent {
|
||||
variables: MobileThemeVariables
|
||||
isSystemTheme: boolean
|
||||
isInitial: boolean
|
||||
luminosity: number
|
||||
isSwapIn?: boolean
|
||||
package_info: ComponentContent['package_info'] & {
|
||||
dock_icon: ThemeDockIcon
|
||||
}
|
||||
}
|
||||
|
||||
export class MobileTheme extends SNTheme {
|
||||
get mobileContent() {
|
||||
return this.content as MobileThemeContent
|
||||
}
|
||||
|
||||
static BuildTheme(variables?: MobileThemeVariables) {
|
||||
const currentDate = new Date()
|
||||
|
||||
return new MobileTheme(
|
||||
new DecryptedPayload({
|
||||
uuid: `${Math.random()}`,
|
||||
content_type: ContentType.Theme,
|
||||
content: FillItemContent<MobileThemeContent>({
|
||||
variables: variables || ({} as MobileThemeVariables),
|
||||
isSystemTheme: false,
|
||||
isInitial: false,
|
||||
}),
|
||||
created_at: currentDate,
|
||||
created_at_timestamp: currentDate.getTime(),
|
||||
updated_at: currentDate,
|
||||
updated_at_timestamp: currentDate.getTime(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
532
packages/mobile/src/Style/ThemeService.ts
Normal file
532
packages/mobile/src/Style/ThemeService.ts
Normal file
@@ -0,0 +1,532 @@
|
||||
import { MobileApplication } from '@Lib/Application'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ComponentContent,
|
||||
ContentType,
|
||||
DecryptedPayload,
|
||||
DecryptedTransferPayload,
|
||||
FillItemContent,
|
||||
removeFromArray,
|
||||
SNTheme,
|
||||
StorageValueModes,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs'
|
||||
import React from 'react'
|
||||
import { Alert, Appearance, Platform, StatusBar, TextStyle, ViewStyle } from 'react-native'
|
||||
import CSSParser from './CssParser'
|
||||
import { MobileTheme } from './MobileTheme'
|
||||
import THEME_DARK_JSON from './Themes/blue-dark.json'
|
||||
import THEME_BLUE_JSON from './Themes/blue.json'
|
||||
import THEME_RED_JSON from './Themes/red.json'
|
||||
import { MobileThemeVariables } from './Themes/styled-components'
|
||||
import { DARK_CONTENT, getColorLuminosity, keyboardColorForTheme, LIGHT_CONTENT, statusBarColorForTheme } from './Utils'
|
||||
|
||||
const LIGHT_THEME_KEY = 'lightThemeKey'
|
||||
const DARK_THEME_KEY = 'darkThemeKey'
|
||||
const CACHED_THEMES_KEY = 'cachedThemesKey'
|
||||
|
||||
type ThemeChangeObserver = () => Promise<void> | void
|
||||
|
||||
type ThemeVariables = Record<string, string>
|
||||
|
||||
enum SystemThemeTint {
|
||||
Blue = 'Blue',
|
||||
Dark = 'Dark',
|
||||
Red = 'Red',
|
||||
}
|
||||
|
||||
export const ThemeServiceContext = React.createContext<ThemeService | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Components might use current theme by using of two ways:
|
||||
* - use ThemeServiceContext
|
||||
* - use current theme injected into styled components
|
||||
*/
|
||||
export class ThemeService {
|
||||
observers: ThemeChangeObserver[] = []
|
||||
private themes: Record<UuidString, MobileTheme> = {}
|
||||
activeThemeId?: string
|
||||
static constants = {
|
||||
mainTextFontSize: 16,
|
||||
paddingLeft: 14,
|
||||
}
|
||||
styles: Record<string, ViewStyle | TextStyle> = {}
|
||||
variables?: MobileThemeVariables
|
||||
application?: MobileApplication
|
||||
unregisterComponentHandler?: () => void
|
||||
unsubscribeStreamThemes?: () => void
|
||||
unsubsribeAppEventObserver?: () => void
|
||||
|
||||
constructor(application: MobileApplication) {
|
||||
this.application = application
|
||||
this.buildSystemThemesAndData()
|
||||
this.registerObservers()
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadCachedThemes()
|
||||
await this.resolveInitialThemeForMode()
|
||||
Appearance.addChangeListener(this.onColorSchemeChange.bind(this))
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.application = undefined
|
||||
Appearance.removeChangeListener(this.onColorSchemeChange.bind(this))
|
||||
this.unregisterComponentHandler && this.unregisterComponentHandler()
|
||||
this.unsubscribeStreamThemes && this.unsubscribeStreamThemes()
|
||||
this.unsubsribeAppEventObserver && this.unsubsribeAppEventObserver()
|
||||
}
|
||||
|
||||
private registerObservers() {
|
||||
this.unsubsribeAppEventObserver = this.application?.addEventObserver(async event => {
|
||||
/**
|
||||
* If there are any migrations we need to set default theme to start UI
|
||||
*/
|
||||
if (event === ApplicationEvent.MigrationsLoaded) {
|
||||
if (await this.application?.hasPendingMigrations()) {
|
||||
this.setDefaultTheme()
|
||||
}
|
||||
}
|
||||
if (event === ApplicationEvent.Launched) {
|
||||
// Resolve initial theme after app launched
|
||||
void this.resolveInitialThemeForMode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private findOrCreateTheme(themeId: string, variables?: MobileThemeVariables) {
|
||||
let theme = this.themes[themeId]
|
||||
if (!theme) {
|
||||
theme = this.buildTheme(undefined, variables)
|
||||
this.addTheme(theme)
|
||||
}
|
||||
return theme
|
||||
}
|
||||
|
||||
private buildSystemThemesAndData() {
|
||||
const themeData = [
|
||||
{
|
||||
uuid: SystemThemeTint.Blue,
|
||||
variables: THEME_BLUE_JSON,
|
||||
name: SystemThemeTint.Blue,
|
||||
isInitial: this.getColorScheme() === 'light',
|
||||
},
|
||||
{
|
||||
uuid: SystemThemeTint.Dark,
|
||||
variables: THEME_DARK_JSON,
|
||||
name: SystemThemeTint.Dark,
|
||||
isInitial: this.getColorScheme() === 'dark',
|
||||
},
|
||||
{
|
||||
uuid: SystemThemeTint.Red,
|
||||
variables: THEME_RED_JSON,
|
||||
name: SystemThemeTint.Red,
|
||||
},
|
||||
]
|
||||
|
||||
for (const option of themeData) {
|
||||
const variables: MobileThemeVariables = {
|
||||
...option.variables,
|
||||
...ThemeService.constants,
|
||||
}
|
||||
variables.statusBar = Platform.OS === 'android' ? LIGHT_CONTENT : DARK_CONTENT
|
||||
|
||||
const currentDate = new Date()
|
||||
const payload = new DecryptedPayload<ComponentContent>({
|
||||
uuid: option.uuid,
|
||||
content_type: ContentType.Theme,
|
||||
content: FillItemContent({
|
||||
package_info: {
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
background_color: variables.stylekitInfoColor,
|
||||
border_color: variables.stylekitInfoColor,
|
||||
},
|
||||
},
|
||||
name: option.name,
|
||||
variables,
|
||||
luminosity: getColorLuminosity(variables.stylekitContrastBackgroundColor),
|
||||
isSystemTheme: true,
|
||||
isInitial: Boolean(option.isInitial),
|
||||
} as unknown as ComponentContent),
|
||||
created_at: currentDate,
|
||||
created_at_timestamp: currentDate.getTime(),
|
||||
updated_at: currentDate,
|
||||
updated_at_timestamp: currentDate.getTime(),
|
||||
})
|
||||
|
||||
const theme = new MobileTheme(payload)
|
||||
|
||||
this.addTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
addTheme(theme: MobileTheme) {
|
||||
this.themes[theme.uuid] = theme
|
||||
}
|
||||
|
||||
public getActiveTheme() {
|
||||
return this.activeThemeId && this.themes[this.activeThemeId]
|
||||
}
|
||||
|
||||
public isLikelyUsingDarkColorTheme() {
|
||||
const activeTheme = this.getActiveTheme()
|
||||
if (!activeTheme) {
|
||||
return false
|
||||
}
|
||||
|
||||
return activeTheme.uuid !== SystemThemeTint.Blue
|
||||
}
|
||||
|
||||
private onColorSchemeChange() {
|
||||
void this.resolveInitialThemeForMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns color scheme or light scheme as a default
|
||||
*/
|
||||
private getColorScheme() {
|
||||
return Appearance.getColorScheme() || 'light'
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an observer for theme change
|
||||
* @returns function that unregisters this observer
|
||||
*/
|
||||
public addThemeChangeObserver(callback: ThemeChangeObserver) {
|
||||
this.observers.push(callback)
|
||||
return () => {
|
||||
removeFromArray(this.observers, callback)
|
||||
}
|
||||
}
|
||||
|
||||
notifyObserversOfThemeChange() {
|
||||
for (const observer of this.observers) {
|
||||
void observer()
|
||||
}
|
||||
}
|
||||
|
||||
public async assignExternalThemeForMode(theme: SNTheme, mode: 'light' | 'dark') {
|
||||
const data = this.findOrCreateTheme(theme.uuid)
|
||||
if (!Object.prototype.hasOwnProperty.call(data, 'variables')) {
|
||||
if ((await this.downloadThemeAndReload(theme)) === false) {
|
||||
return
|
||||
}
|
||||
}
|
||||
void this.assignThemeForMode(theme.uuid, mode)
|
||||
}
|
||||
|
||||
public async assignThemeForMode(themeId: string, mode: 'light' | 'dark') {
|
||||
void this.setThemeForMode(mode, themeId)
|
||||
|
||||
/**
|
||||
* If we're changing the theme for a specific mode and we're currently on
|
||||
* that mode, then activate this theme.
|
||||
*/
|
||||
if (this.getColorScheme() === mode && this.activeThemeId !== themeId) {
|
||||
this.activateTheme(themeId)
|
||||
}
|
||||
}
|
||||
|
||||
private async setThemeForMode(mode: 'light' | 'dark', themeId: string) {
|
||||
return this.application?.setValue(
|
||||
mode === 'dark' ? DARK_THEME_KEY : LIGHT_THEME_KEY,
|
||||
themeId,
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
}
|
||||
|
||||
public async getThemeForMode(mode: 'light' | 'dark') {
|
||||
return this.application?.getValue(mode === 'dark' ? DARK_THEME_KEY : LIGHT_THEME_KEY, StorageValueModes.Nonwrapped)
|
||||
}
|
||||
|
||||
/**
|
||||
* When downloading an external theme, we can't depend on it having all the
|
||||
* variables present. So we will merge them with this template variable list
|
||||
* to make sure the end result has all variables the app expects. Return a
|
||||
* copy as the result may be modified before use.
|
||||
*/
|
||||
templateVariables() {
|
||||
return Object.assign({}, THEME_BLUE_JSON) as MobileThemeVariables
|
||||
}
|
||||
|
||||
private setDefaultTheme() {
|
||||
const defaultThemeId = this.getColorScheme() === 'dark' ? SystemThemeTint.Dark : SystemThemeTint.Blue
|
||||
|
||||
this.setActiveTheme(defaultThemeId)
|
||||
}
|
||||
|
||||
private async resolveInitialThemeForMode() {
|
||||
try {
|
||||
const savedThemeId = await this.getThemeForMode(this.getColorScheme())
|
||||
const matchingThemeId = Object.keys(this.themes).find(themeId => themeId === savedThemeId)
|
||||
if (matchingThemeId) {
|
||||
this.setActiveTheme(matchingThemeId)
|
||||
void this.application?.mobileComponentManager.preloadThirdPartyThemeIndexPath()
|
||||
} else {
|
||||
this.setDefaultTheme()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing initial theme', e)
|
||||
return this.setDefaultTheme()
|
||||
}
|
||||
}
|
||||
|
||||
keyboardColorForActiveTheme() {
|
||||
return keyboardColorForTheme(this.findOrCreateTheme(this.activeThemeId!))
|
||||
}
|
||||
|
||||
systemThemes() {
|
||||
return Object.values(this.themes)
|
||||
.filter(theme => theme.mobileContent.isSystemTheme)
|
||||
.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
nonSystemThemes() {
|
||||
return Object.values(this.themes)
|
||||
.filter(theme => !theme.mobileContent.isSystemTheme)
|
||||
.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
private buildTheme(base?: MobileTheme, baseVariables?: MobileThemeVariables) {
|
||||
const theme = base || MobileTheme.BuildTheme()
|
||||
/** Merge default variables to ensure this theme has all the variables. */
|
||||
const variables = {
|
||||
...this.templateVariables(),
|
||||
...theme.mobileContent.variables,
|
||||
...baseVariables,
|
||||
}
|
||||
const luminosity = theme.mobileContent.luminosity || getColorLuminosity(variables.stylekitContrastBackgroundColor)
|
||||
return new MobileTheme(
|
||||
theme.payload.copy({
|
||||
content: {
|
||||
...theme.mobileContent,
|
||||
variables,
|
||||
luminosity,
|
||||
} as unknown as ComponentContent,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
setActiveTheme(themeId: string) {
|
||||
const theme = this.findOrCreateTheme(themeId)
|
||||
const updatedTheme = this.buildTheme(theme)
|
||||
this.addTheme(updatedTheme)
|
||||
this.variables = updatedTheme.mobileContent.variables
|
||||
if (this.application?.isLaunched() && this.application.componentManager) {
|
||||
this.application.mobileComponentManager.setMobileActiveTheme(updatedTheme)
|
||||
}
|
||||
this.activeThemeId = themeId
|
||||
this.updateDeviceForTheme(themeId)
|
||||
this.notifyObserversOfThemeChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates local device items for newly activated theme.
|
||||
*
|
||||
* This includes:
|
||||
* - Status Bar color
|
||||
*/
|
||||
private updateDeviceForTheme(themeId: string) {
|
||||
const theme = this.findOrCreateTheme(themeId)
|
||||
const isAndroid = Platform.OS === 'android'
|
||||
/** On Android, a time out is required, especially during app startup. */
|
||||
setTimeout(
|
||||
() => {
|
||||
const statusBarColor = statusBarColorForTheme(theme)
|
||||
StatusBar.setBarStyle(statusBarColor, true)
|
||||
|
||||
if (isAndroid) {
|
||||
/**
|
||||
* Android <= v22 does not support changing status bar text color.
|
||||
* It will always be white, so we have to make sure background color
|
||||
* has proper contrast.
|
||||
*/
|
||||
if (Platform.Version <= 22) {
|
||||
StatusBar.setBackgroundColor('#000000')
|
||||
} else {
|
||||
StatusBar.setBackgroundColor(theme.mobileContent.variables.stylekitContrastBackgroundColor)
|
||||
}
|
||||
}
|
||||
},
|
||||
isAndroid ? 100 : 0,
|
||||
)
|
||||
}
|
||||
|
||||
private async downloadTheme(theme: SNTheme): Promise<ThemeVariables | undefined> {
|
||||
const componentManager = this.application?.mobileComponentManager
|
||||
if (componentManager?.isComponentDownloadable(theme)) {
|
||||
if (await componentManager.doesComponentNeedDownload(theme)) {
|
||||
await componentManager.downloadComponentOffline(theme)
|
||||
}
|
||||
|
||||
const file = await componentManager.getIndexFile(theme.identifier)
|
||||
if (!file) {
|
||||
console.error(`Did not find local index file for ${theme.identifier}`)
|
||||
return undefined
|
||||
}
|
||||
const variables: ThemeVariables = CSSParser.cssToObject(file)
|
||||
if (!variables || Object.keys(variables).length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return variables
|
||||
}
|
||||
|
||||
let url = theme.hosted_url
|
||||
if (!url) {
|
||||
console.error('Theme download error')
|
||||
return
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android' && url.includes('localhost')) {
|
||||
url = url.replace('localhost', '10.0.2.2')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url!, {
|
||||
method: 'GET',
|
||||
})
|
||||
const data = await response.text()
|
||||
const variables: ThemeVariables = CSSParser.cssToObject(data)
|
||||
if (!variables || Object.keys(variables).length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return variables
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
activateSystemTheme(themeId: string) {
|
||||
this.activateTheme(themeId)
|
||||
}
|
||||
|
||||
async activateExternalTheme(theme: SNTheme) {
|
||||
const existing = this.themes[theme.uuid]
|
||||
if (existing && existing.mobileContent.variables) {
|
||||
this.activateTheme(theme.uuid)
|
||||
return
|
||||
}
|
||||
const variables = await this.downloadTheme(theme)
|
||||
if (!variables) {
|
||||
Alert.alert('Not Available', 'This theme is not available on mobile.')
|
||||
return
|
||||
}
|
||||
const appliedVariables = Object.assign(this.templateVariables(), variables)
|
||||
const finalVariables = {
|
||||
...appliedVariables,
|
||||
...ThemeService.constants,
|
||||
}
|
||||
const mobileTheme = new MobileTheme(
|
||||
theme.payload.copy({
|
||||
content: {
|
||||
...theme.payload.content,
|
||||
variables: finalVariables,
|
||||
luminosity: getColorLuminosity(finalVariables.stylekitContrastBackgroundColor),
|
||||
isSystemTheme: false,
|
||||
isInitial: false,
|
||||
package_info: {
|
||||
...theme.payload.content.package_info,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
background_color: finalVariables.stylekitInfoColor,
|
||||
border_color: finalVariables.stylekitInfoColor,
|
||||
},
|
||||
},
|
||||
} as unknown as ComponentContent,
|
||||
}),
|
||||
)
|
||||
this.addTheme(mobileTheme)
|
||||
this.activateTheme(theme.uuid)
|
||||
void this.cacheThemes()
|
||||
}
|
||||
|
||||
private activateTheme(themeId: string) {
|
||||
this.setActiveTheme(themeId)
|
||||
void this.assignThemeForMode(themeId, this.getColorScheme())
|
||||
}
|
||||
|
||||
private async loadCachedThemes() {
|
||||
const rawValue = (await this.application!.getValue(CACHED_THEMES_KEY, StorageValueModes.Nonwrapped)) || []
|
||||
|
||||
const themes = (rawValue as DecryptedTransferPayload<ComponentContent>[]).map(rawPayload => {
|
||||
const payload = new DecryptedPayload<ComponentContent>(rawPayload)
|
||||
|
||||
return new MobileTheme(payload)
|
||||
})
|
||||
|
||||
for (const theme of themes) {
|
||||
this.addTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheThemes() {
|
||||
const themes = this.nonSystemThemes()
|
||||
return this.application!.setValue(
|
||||
CACHED_THEMES_KEY,
|
||||
themes.map(t => t.payloadRepresentation()),
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
}
|
||||
|
||||
public async downloadThemeAndReload(theme: SNTheme) {
|
||||
const variables = await this.downloadTheme(theme)
|
||||
if (!variables) {
|
||||
return false
|
||||
}
|
||||
/** Merge default variables to ensure this theme has all the variables. */
|
||||
const appliedVariables = Object.assign(this.templateVariables(), variables)
|
||||
const mobileTheme = this.findOrCreateTheme(theme.uuid, {
|
||||
...appliedVariables,
|
||||
...ThemeService.constants,
|
||||
})
|
||||
this.addTheme(mobileTheme)
|
||||
void this.cacheThemes()
|
||||
if (theme.uuid === this.activeThemeId) {
|
||||
this.setActiveTheme(theme.uuid)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static doesDeviceSupportDarkMode() {
|
||||
/**
|
||||
* Android supportsDarkMode relies on a Configuration value in the API
|
||||
* that was not available until Android 8.0 (26)
|
||||
* https://developer.android.com/reference/android/content/res/Configuration#UI_MODE_NIGHT_UNDEFINED
|
||||
* iOS supports Dark mode from iOS 13
|
||||
*/
|
||||
|
||||
if (Platform.OS === 'android' && Platform.Version < 26) {
|
||||
return false
|
||||
} else if (Platform.OS === 'ios') {
|
||||
const majorVersionIOS = parseInt(Platform.Version as string, 10)
|
||||
return majorVersionIOS >= 13
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
static platformIconPrefix() {
|
||||
return Platform.OS === 'android' ? 'md' : 'ios'
|
||||
}
|
||||
|
||||
static nameForIcon(iconName: string) {
|
||||
return ThemeService.platformIconPrefix() + '-' + iconName
|
||||
}
|
||||
}
|
||||
50
packages/mobile/src/Style/Themes/blue-dark.json
Normal file
50
packages/mobile/src/Style/Themes/blue-dark.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"stylekitBaseFontSize": "14px",
|
||||
"stylekitFontSizeP": "1.0rem",
|
||||
"stylekitFontSizeEditor": "1.21rem",
|
||||
"stylekitFontSizeH6": "0.8rem",
|
||||
"stylekitFontSizeH5": "0.9rem",
|
||||
"stylekitFontSizeH4": "1.0rem",
|
||||
"stylekitFontSizeH3": "1.1rem",
|
||||
"stylekitFontSizeH2": "1.2rem",
|
||||
"stylekitFontSizeH1": "1.3rem",
|
||||
"stylekitNeutralColor": "#C4C4C4",
|
||||
"stylekitNeutralContrastColor": "#ffffff",
|
||||
"stylekitInfoColor": "#749BDA",
|
||||
"stylekitInfoContrastColor": "#121212",
|
||||
"stylekitSuccessColor": "#82AD6F",
|
||||
"stylekitSuccessContrastColor": "#ffffff",
|
||||
"stylekitWarningColor": "#E0934D",
|
||||
"stylekitWarningContrastColor": "#ffffff",
|
||||
"stylekitDangerColor": "#CE7E7E",
|
||||
"stylekitDangerContrastColor": "#ffffff",
|
||||
"stylekitShadowColor": "#20202b",
|
||||
"stylekitBackgroundColor": "#121212",
|
||||
"stylekitBorderColor": "#2E2E2E",
|
||||
"stylekitForegroundColor": "#E0E0E0",
|
||||
"stylekitContrastBackgroundColor": "#2E2E2E",
|
||||
"stylekitContrastForegroundColor": "#E0E0E0",
|
||||
"stylekitContrastBorderColor": "#2E2E2E",
|
||||
"stylekitSecondaryBackgroundColor": "#121212",
|
||||
"stylekitSecondaryForegroundColor": "#E0E0E0",
|
||||
"stylekitSecondaryBorderColor": "#2E2E2E",
|
||||
"stylekitSecondaryContrastBackgroundColor": "#2E2E2E",
|
||||
"stylekitSecondaryContrastForegroundColor": "#E0E0E0",
|
||||
"stylekitEditorBackgroundColor": "#121212",
|
||||
"stylekitEditorForegroundColor": "#E0E0E0",
|
||||
"stylekitParagraphTextColor": "#454545",
|
||||
"stylekitInputPlaceholderColor": "rgb(168, 168, 168)",
|
||||
"stylekitInputBorderColor": "#2E2E2E",
|
||||
"stylekitScrollbarThumbColor": "#749BDA",
|
||||
"stylekitScrollbarTrackBorderColor": "#2E2E2E",
|
||||
"stylekitPalSky": "#72767E",
|
||||
"stylekitCorn": "#EBAD00",
|
||||
"stylekitDeepBlush": "#EA6595",
|
||||
"stylekitPurpleHeart": "#7049CF",
|
||||
"stylekitMountainMeadow": "#1AA772",
|
||||
"stylekitJaffa": "#F28C52",
|
||||
"stylekitAbbey": "#515357",
|
||||
"stylekitCodGray": "#1C1C1C",
|
||||
"stylekitIron": "#DFE1E4",
|
||||
"statusBar": ""
|
||||
}
|
||||
50
packages/mobile/src/Style/Themes/blue.json
Normal file
50
packages/mobile/src/Style/Themes/blue.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"stylekitBaseFontSize": "14px",
|
||||
"stylekitFontSizeP": "1.0rem",
|
||||
"stylekitFontSizeEditor": "1.21rem",
|
||||
"stylekitFontSizeH6": "0.8rem",
|
||||
"stylekitFontSizeH5": "0.9rem",
|
||||
"stylekitFontSizeH4": "1.0rem",
|
||||
"stylekitFontSizeH3": "1.1rem",
|
||||
"stylekitFontSizeH2": "1.2rem",
|
||||
"stylekitFontSizeH1": "1.3rem",
|
||||
"stylekitNeutralColor": "#989898",
|
||||
"stylekitNeutralContrastColor": "#ffffff",
|
||||
"stylekitInfoColor": "#086DD6",
|
||||
"stylekitInfoContrastColor": "#ffffff",
|
||||
"stylekitSuccessColor": "#2B9612",
|
||||
"stylekitSuccessContrastColor": "#ffffff",
|
||||
"stylekitWarningColor": "#f6a200",
|
||||
"stylekitWarningContrastColor": "#ffffff",
|
||||
"stylekitDangerColor": "#F80324",
|
||||
"stylekitDangerContrastColor": "#ffffff",
|
||||
"stylekitShadowColor": "#C8C8C8",
|
||||
"stylekitBackgroundColor": "#ffffff",
|
||||
"stylekitBorderColor": "#e3e3e3",
|
||||
"stylekitForegroundColor": "black",
|
||||
"stylekitContrastBackgroundColor": "#F6F6F6",
|
||||
"stylekitContrastForegroundColor": "#2e2e2e",
|
||||
"stylekitContrastBorderColor": "#e3e3e3",
|
||||
"stylekitSecondaryBackgroundColor": "#F6F6F6",
|
||||
"stylekitSecondaryForegroundColor": "#2e2e2e",
|
||||
"stylekitSecondaryBorderColor": "#e3e3e3",
|
||||
"stylekitSecondaryContrastBackgroundColor": "#e3e3e3",
|
||||
"stylekitSecondaryContrastForegroundColor": "#2e2e2e",
|
||||
"stylekitEditorBackgroundColor": "#ffffff",
|
||||
"stylekitEditorForegroundColor": "black",
|
||||
"stylekitParagraphTextColor": "#454545",
|
||||
"stylekitInputPlaceholderColor": "rgb(168, 168, 168)",
|
||||
"stylekitInputBorderColor": "#e3e3e3",
|
||||
"stylekitScrollbarThumbColor": "#dfdfdf",
|
||||
"stylekitScrollbarTrackBorderColor": "#E7E7E7",
|
||||
"stylekitPalSky": "#72767E",
|
||||
"stylekitCorn": "#EBAD00",
|
||||
"stylekitDeepBlush": "#EA6595",
|
||||
"stylekitPurpleHeart": "#7049CF",
|
||||
"stylekitMountainMeadow": "#1AA772",
|
||||
"stylekitJaffa": "#F28C52",
|
||||
"stylekitAbbey": "#515357",
|
||||
"stylekitCodGray": "#1C1C1C",
|
||||
"stylekitIron": "#DFE1E4",
|
||||
"statusBar": ""
|
||||
}
|
||||
50
packages/mobile/src/Style/Themes/red.json
Normal file
50
packages/mobile/src/Style/Themes/red.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"stylekitBaseFontSize": "14px",
|
||||
"stylekitFontSizeP": "1.0rem",
|
||||
"stylekitFontSizeEditor": "1.21rem",
|
||||
"stylekitFontSizeH6": "0.8rem",
|
||||
"stylekitFontSizeH5": "0.9rem",
|
||||
"stylekitFontSizeH4": "1.0rem",
|
||||
"stylekitFontSizeH3": "1.1rem",
|
||||
"stylekitFontSizeH2": "1.2rem",
|
||||
"stylekitFontSizeH1": "1.3rem",
|
||||
"stylekitNeutralColor": "#989898",
|
||||
"stylekitNeutralContrastColor": "#ffffff",
|
||||
"stylekitInfoColor": "#fb0206",
|
||||
"stylekitInfoContrastColor": "#ffffff",
|
||||
"stylekitSuccessColor": "#2B9612",
|
||||
"stylekitSuccessContrastColor": "#ffffff",
|
||||
"stylekitWarningColor": "#f6a200",
|
||||
"stylekitWarningContrastColor": "#ffffff",
|
||||
"stylekitDangerColor": "#F80324",
|
||||
"stylekitDangerContrastColor": "#ffffff",
|
||||
"stylekitShadowColor": "#C8C8C8",
|
||||
"stylekitBackgroundColor": "#ffffff",
|
||||
"stylekitBorderColor": "#e3e3e3",
|
||||
"stylekitForegroundColor": "black",
|
||||
"stylekitContrastBackgroundColor": "#F6F6F6",
|
||||
"stylekitContrastForegroundColor": "#2e2e2e",
|
||||
"stylekitContrastBorderColor": "#e3e3e3",
|
||||
"stylekitSecondaryBackgroundColor": "#F6F6F6",
|
||||
"stylekitSecondaryForegroundColor": "#2e2e2e",
|
||||
"stylekitSecondaryBorderColor": "#e3e3e3",
|
||||
"stylekitSecondaryContrastBackgroundColor": "#e3e3e3",
|
||||
"stylekitSecondaryContrastForegroundColor": "#2e2e2e",
|
||||
"stylekitEditorBackgroundColor": "#ffffff",
|
||||
"stylekitEditorForegroundColor": "black",
|
||||
"stylekitParagraphTextColor": "#454545",
|
||||
"stylekitInputPlaceholderColor": "rgb(168, 168, 168)",
|
||||
"stylekitInputBorderColor": "#e3e3e3",
|
||||
"stylekitScrollbarThumbColor": "#dfdfdf",
|
||||
"stylekitScrollbarTrackBorderColor": "#E7E7E7",
|
||||
"stylekitPalSky": "#72767E",
|
||||
"stylekitCorn": "#EBAD00",
|
||||
"stylekitDeepBlush": "#EA6595",
|
||||
"stylekitPurpleHeart": "#7049CF",
|
||||
"stylekitMountainMeadow": "#1AA772",
|
||||
"stylekitJaffa": "#F28C52",
|
||||
"stylekitAbbey": "#515357",
|
||||
"stylekitCodGray": "#1C1C1C",
|
||||
"stylekitIron": "#DFE1E4",
|
||||
"statusBar": ""
|
||||
}
|
||||
11
packages/mobile/src/Style/Themes/styled-components.d.ts
vendored
Normal file
11
packages/mobile/src/Style/Themes/styled-components.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import theme from './blue.json'
|
||||
|
||||
export type MobileThemeVariables = typeof theme & {
|
||||
paddingLeft: number
|
||||
mainTextFontSize: number
|
||||
}
|
||||
|
||||
declare module 'styled-components' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface DefaultTheme extends MobileThemeVariables {}
|
||||
}
|
||||
145
packages/mobile/src/Style/Utils.ts
Normal file
145
packages/mobile/src/Style/Utils.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { isNullOrUndefined } from '@standardnotes/snjs'
|
||||
import { Platform, ScaledSize } from 'react-native'
|
||||
import { DefaultTheme } from 'styled-components/native'
|
||||
import { MobileTheme } from './MobileTheme'
|
||||
/* eslint-disable no-bitwise */
|
||||
export const LIGHT_MODE_KEY = 'light'
|
||||
export const DARK_MODE_KEY = 'dark'
|
||||
export const LIGHT_CONTENT = 'light-content'
|
||||
export const DARK_CONTENT = 'dark-content'
|
||||
|
||||
export const getDefaultDrawerWidth = ({ height, width }: ScaledSize) => {
|
||||
/*
|
||||
* Default drawer width is screen width - header height
|
||||
* with a max width of 280 on mobile and 320 on tablet
|
||||
* https://material.io/guidelines/patterns/navigation-drawer.html
|
||||
*/
|
||||
const smallerAxisSize = Math.min(height, width)
|
||||
const isLandscape = width > height
|
||||
const isTablet = smallerAxisSize >= 600
|
||||
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56
|
||||
const maxWidth = isTablet ? 320 : 280
|
||||
|
||||
return Math.min(smallerAxisSize - appBarHeight, maxWidth)
|
||||
}
|
||||
|
||||
export function statusBarColorForTheme(theme: MobileTheme) {
|
||||
if (isNullOrUndefined(theme.mobileContent.luminosity)) {
|
||||
throw Error('Theme luminocity should not be null')
|
||||
}
|
||||
// The main nav bar uses contrast background color
|
||||
if (theme.mobileContent.luminosity < 130) {
|
||||
// is dark color, return white status bar
|
||||
return LIGHT_CONTENT
|
||||
} else {
|
||||
return DARK_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
export function keyboardColorForTheme(theme: MobileTheme) {
|
||||
if (isNullOrUndefined(theme.mobileContent.luminosity)) {
|
||||
throw Error('Theme luminocity should not be null')
|
||||
}
|
||||
if (theme.mobileContent.luminosity < 130) {
|
||||
// is dark color, return dark keyboard
|
||||
return DARK_MODE_KEY
|
||||
} else {
|
||||
return LIGHT_MODE_KEY
|
||||
}
|
||||
}
|
||||
|
||||
export function getColorLuminosity(hexCode: string) {
|
||||
let c = hexCode
|
||||
c = c.substring(1) // strip #
|
||||
const rgb = parseInt(c, 16) // convert rrggbb to decimal
|
||||
const r = (rgb >> 16) & 0xff // extract red
|
||||
const g = (rgb >> 8) & 0xff // extract green
|
||||
const b = (rgb >> 0) & 0xff // extract blue
|
||||
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b // per ITU-R BT.709
|
||||
}
|
||||
|
||||
export function shadeBlend(p: number, c0: string, c1?: string) {
|
||||
const n = p < 0 ? p * -1 : p,
|
||||
u = Math.round,
|
||||
w = parseInt
|
||||
if (c0.length > 7) {
|
||||
const f = c0.split(','),
|
||||
t = (c1 ? c1 : p < 0 ? 'rgb(0,0,0)' : 'rgb(255,255,255)').split(','),
|
||||
R = w(f[0].slice(4)),
|
||||
G = w(f[1]),
|
||||
B = w(f[2])
|
||||
return (
|
||||
'rgb(' +
|
||||
(u((w(t[0].slice(4)) - R) * n) + R) +
|
||||
',' +
|
||||
(u((w(t[1]) - G) * n) + G) +
|
||||
',' +
|
||||
(u((w(t[2]) - B) * n) + B) +
|
||||
')'
|
||||
)
|
||||
} else {
|
||||
const f = w(c0.slice(1), 16),
|
||||
t = w((c1 ? c1 : p < 0 ? '#000000' : '#FFFFFF').slice(1), 16),
|
||||
R1 = f >> 16,
|
||||
G1 = (f >> 8) & 0x00ff,
|
||||
B1 = f & 0x0000ff
|
||||
return (
|
||||
'#' +
|
||||
(
|
||||
0x1000000 +
|
||||
(u(((t >> 16) - R1) * n) + R1) * 0x10000 +
|
||||
(u((((t >> 8) & 0x00ff) - G1) * n) + G1) * 0x100 +
|
||||
(u(((t & 0x0000ff) - B1) * n) + B1)
|
||||
)
|
||||
.toString(16)
|
||||
.slice(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function darken(color: string, value = -0.15) {
|
||||
return shadeBlend(value, color)
|
||||
}
|
||||
|
||||
export function lighten(color: string, value = 0.25) {
|
||||
return shadeBlend(value, color)
|
||||
}
|
||||
|
||||
export function hexToRGBA(hex: string, alpha: number) {
|
||||
if (!hex || !hex.startsWith('#')) {
|
||||
return ''
|
||||
}
|
||||
let c: any
|
||||
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
|
||||
c = hex.substring(1).split('')
|
||||
if (c.length === 3) {
|
||||
c = [c[0], c[0], c[1], c[1], c[2], c[2]]
|
||||
}
|
||||
c = '0x' + c.join('')
|
||||
return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ',' + alpha + ')'
|
||||
} else {
|
||||
throw new Error('Bad Hex')
|
||||
}
|
||||
}
|
||||
|
||||
export const getTintColorForEditor = (theme: DefaultTheme, tint: number): string | undefined => {
|
||||
const {
|
||||
stylekitInfoColor,
|
||||
stylekitDeepBlush,
|
||||
stylekitCorn,
|
||||
stylekitPurpleHeart,
|
||||
stylekitMountainMeadow,
|
||||
stylekitJaffa,
|
||||
} = theme
|
||||
|
||||
const tintColorsMap = new Map([
|
||||
[1, stylekitInfoColor],
|
||||
[2, stylekitDeepBlush],
|
||||
[3, stylekitCorn],
|
||||
[4, stylekitPurpleHeart],
|
||||
[5, stylekitMountainMeadow],
|
||||
[6, stylekitJaffa],
|
||||
])
|
||||
return tintColorsMap.get(tint)
|
||||
}
|
||||
23
packages/mobile/src/Style/android_text_fix.js
Normal file
23
packages/mobile/src/Style/android_text_fix.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { Platform, StyleSheet, Text } from 'react-native'
|
||||
|
||||
export function enableAndroidFontFix() {
|
||||
// Bail if this isn't an Android device
|
||||
if (Platform.OS !== 'android') {
|
||||
return
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
fontFamily: 'Roboto',
|
||||
},
|
||||
})
|
||||
|
||||
let __render = Text.render
|
||||
Text.render = function (...args) {
|
||||
let origin = __render.call(this, ...args)
|
||||
return React.cloneElement(origin, {
|
||||
style: [styles.text, origin.props.style],
|
||||
})
|
||||
}
|
||||
}
|
||||
3
packages/mobile/src/Style/package.json
Normal file
3
packages/mobile/src/Style/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "@Style"
|
||||
}
|
||||
Reference in New Issue
Block a user