UNPKG

react-native-themex

Version:

A simple and effective implementation to manage themes on react native apps

168 lines (127 loc) 6 kB
import { useState, useEffect } from 'react' import { BehaviorSubject } from "rxjs" import { Appearance } from 'react-native' import AsyncStorage from '@react-native-community/async-storage' function changeNavigationBarColor(color: string, light: boolean, animated: boolean): void { try { require('react-native-navigation-bar-color').default(color, light, animated) } catch(e) { if (e.code === "MODULE_NOT_FOUND" || e.name === "MODULE_NOT_FOUND") console.warn('react-native-themex: the "react-native-navigation-bar-color" has to be installed and configured to be able to change the android navigation bar color') else throw e; } } export interface Theme { name: string androidNavigationBarColor?: string androidNavigationBarColorScheme?: 'light' | 'dark' [key: string]: any } export interface ThemeCollection<T extends Theme = Theme> { [key: string]: T } const Themes: ThemeCollection = {} const colorSchemeToThemeName = function(){ let _default: string, _light: string, _dark: string return Object.seal({ get default() { return _default }, set default(themeName: string) { _default = themeName }, get light() { return _light }, set light(themeName: string) { _light = themeName }, get dark() { return _dark }, set dark(themeName: string) { _dark = themeName }, get system() { const systemColorScheme = Appearance.getColorScheme() if (systemColorScheme === 'light') return _light else if (systemColorScheme === 'dark') return _dark else return _default } }) }() export function registerThemes<T extends Theme>( ...themes: ([T, 'light' | 'dark' | 'default' | undefined | null] | T)[] ) { if (!themes.length) throw new Error('react-native-theme: you have to register at least one theme') themes.forEach((themeInfo) => { let theme: T, colorScheme: 'light' | 'dark' | 'default' | undefined | null if (themeInfo instanceof Array) [theme, colorScheme] = themeInfo else theme = themeInfo Themes[theme.name] = theme if (colorScheme) colorSchemeToThemeName[colorScheme] = theme.name if (!THEME_NAME.value) setTHEME_NAME(theme.name, { save: false, shouldChangeNavigationBarColor: false }) }) if (!colorSchemeToThemeName.default) colorSchemeToThemeName.default = themes[0][0].name if (!colorSchemeToThemeName.light) colorSchemeToThemeName.light = themes[0][0].name if (!colorSchemeToThemeName.dark) colorSchemeToThemeName.dark = themes[0][0].name if (!THEME_NAME.value || THEME_NAME.value === themes[0][0].name) { AsyncStorage.getItem('react-native-theme/theme-name').then(themeName => { if (themeName && Themes[themeName] && themeName !== THEME_NAME.value) { setTHEME_NAME(themeName, { save: false }) return false } else return true }).then(shouldChangeNavigationBarColor => { if (themes[0][0].androidNavigationBarColor && shouldChangeNavigationBarColor) { changeNavigationBarColor( themes[0][0].androidNavigationBarColor, themes[0][0].androidNavigationBarColorScheme === 'light' ? true : false, false ) } }) } } export function theme<T extends Theme>(theme: T, colorScheme?: 'light' | 'dark' | 'default'): [T, 'light' | 'dark' | 'default' | undefined | null] { return [theme, colorScheme] } const THEME_NAME = new BehaviorSubject<string|undefined>(undefined) function setTHEME_NAME(themeName: string, { save = true, changeNavigationBarColorAnimated = false, shouldChangeNavigationBarColor = true } = {}) { if (!Themes[themeName]) throw new Error(`react-native-theme: trying to set a theme that wasn't registered: "${themeName}"`) if (save) AsyncStorage.setItem('react-native-theme/theme-name', themeName) if (themeName !== THEME_NAME.value) { THEME_NAME.next(themeName) const theme = Themes[themeName] if (theme.androidNavigationBarColor && shouldChangeNavigationBarColor) changeNavigationBarColor(theme.androidNavigationBarColor, theme.androidNavigationBarColorScheme === 'light' ? true : false, changeNavigationBarColorAnimated) } } export function useTheme<T extends Theme = Theme>() { const [themeName, setThemeName] = useState(THEME_NAME.value) useEffect(() => { const subscription = THEME_NAME.subscribe(setThemeName) return () => subscription.unsubscribe() }, []) if (!themeName) throw new Error('react-native-theme: you have to register at least one theme') else return Themes[themeName] as T } export function getTheme<T extends Theme = Theme>(): T { if (!THEME_NAME.value) throw new Error('react-native-theme: you have to register at least one theme') else return Themes[THEME_NAME.value] as T } export function setTheme(themeName: string) { setTHEME_NAME(themeName, { changeNavigationBarColorAnimated: true }) } export function getThemes<T extends Theme = Theme>() { return Object.freeze({ ...Themes }) as ThemeCollection<T> } const LOADING = new BehaviorSubject(!THEME_NAME.value) const loadingSubscription = THEME_NAME.subscribe(themeName => { LOADING.next(!themeName) if (themeName) loadingSubscription.unsubscribe() LOADING.complete() }) export function themeIsLoading() { return LOADING.value } export function useThemeLoading() { const [isLoading, setIsLoading] = useState(LOADING.value) useEffect(() => { const subscription = LOADING.subscribe(loading => { setIsLoading(loading) if (!loading) subscription.unsubscribe() }) return () => { if (!subscription.closed) subscription.unsubscribe() } }, []) return isLoading }