feat: mobile app package (#1075)

This commit is contained in:
Mo
2022-06-09 09:45:15 -05:00
committed by GitHub
parent 58b63898de
commit 8248a38280
336 changed files with 47696 additions and 22563 deletions

View 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)
}
}

View 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 }
}

View 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'

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View 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(),
}),
)
}
}

View 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
}
}

View 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": ""
}

View 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": ""
}

View 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": ""
}

View 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 {}
}

View 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)
}

View 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],
})
}
}

View File

@@ -0,0 +1,3 @@
{
"name": "@Style"
}