From dfcab67264be28fbdeff7ba6c169a445b3894ae6 Mon Sep 17 00:00:00 2001 From: Radek Czemerys Date: Wed, 19 Aug 2020 22:11:22 +0200 Subject: [PATCH] feature: WIP themes --- package.json | 2 +- src/App.tsx | 39 +++-- .../Settings/Sections/CompanySection.tsx | 1 + src/screens/SideMenu/MainSideMenu.tsx | 108 +++++++++++-- src/screens/SideMenu/TagSelectionList.tsx | 15 +- src/style/StyleKit.ts | 151 +++++++++++------- src/style/Util/ThemeDownloader.ts | 44 ----- yarn.lock | 4 +- 8 files changed, 222 insertions(+), 142 deletions(-) delete mode 100644 src/style/Util/ThemeDownloader.ts diff --git a/package.json b/package.json index 5f4135b6..117010a9 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "react-native-webview": "^10.7.0", "react-navigation-header-buttons": "^5.0.2", "sn-textview": "standardnotes/sn-textview#f42f0bf", - "snjs": "standardnotes/snjs#84b0403", + "snjs": "standardnotes/snjs#27688ba", "standard-notes-rn": "standardnotes/standard-notes-rn", "styled-components": "^5.1.1" }, diff --git a/src/App.tsx b/src/App.tsx index 50379181..4741861f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,6 +29,7 @@ import { MainSideMenu } from '@Screens/SideMenu/MainSideMenu'; import { NoteSideMenu } from '@Screens/SideMenu/NoteSideMenu'; import { ICON_CHECKMARK, ICON_CLOSE, ICON_MENU } from '@Style/icons'; import { StyleKit, StyleKitContext } from '@Style/StyleKit'; +import { StyleKitTheme } from '@Style/Themes/styled-components'; import { getDefaultDrawerWidth } from '@Style/Util/getDefaultDraerWidth'; import React, { useCallback, @@ -420,35 +421,55 @@ const AppComponent: React.FC<{ env: 'prod' | 'dev'; }> = ({ application, env }) => { const [ready, setReady] = useState(false); - const styleKit = useRef(undefined); + const styleKit = useRef(); + const [activeTheme, setActiveTheme] = useState(); + + const setStyleKitRef = useCallback((node: StyleKit | undefined) => { + if (node) { + node.addThemeChangeObserver(() => { + setActiveTheme(node.theme); + }); + } + + // Save a reference to the node + styleKit.current = node; + }, []); useEffect(() => { + let styleKitInstance: StyleKit; const loadApplication = async () => { await application?.prepareForLaunch({ receiveChallenge: async challenge => { application!.promptForChallenge(challenge); }, }); - styleKit.current = new StyleKit(application); - await styleKit.current.init(); + styleKitInstance = new StyleKit(application); + await styleKitInstance.init(); + setStyleKitRef(styleKitInstance); + setActiveTheme(styleKitInstance.theme); setReady(true); }; setReady(false); loadApplication(); - }, [application, env]); - if (!ready || !styleKit.current) { + return () => { + styleKitInstance?.deinit(); + setStyleKitRef(undefined); + }; + }, [application, env, setStyleKitRef]); + + if (!ready || !styleKit.current || !activeTheme) { return null; } - // TODO: better modes support + return ( { @@ -459,7 +480,7 @@ const AppComponent: React.FC<{ {styleKit.current && ( <> - + diff --git a/src/screens/Settings/Sections/CompanySection.tsx b/src/screens/Settings/Sections/CompanySection.tsx index a21b82fb..a7fc6a20 100644 --- a/src/screens/Settings/Sections/CompanySection.tsx +++ b/src/screens/Settings/Sections/CompanySection.tsx @@ -65,6 +65,7 @@ export const CompanySection = (props: Props) => { openUrl('help')} diff --git a/src/screens/SideMenu/MainSideMenu.tsx b/src/screens/SideMenu/MainSideMenu.tsx index c6072540..22238eee 100644 --- a/src/screens/SideMenu/MainSideMenu.tsx +++ b/src/screens/SideMenu/MainSideMenu.tsx @@ -3,9 +3,11 @@ import { useNavigation } from '@react-navigation/native'; import { ApplicationContext } from '@Root/ApplicationContext'; import { SCREEN_SETTINGS } from '@Screens/screens'; import { ICON_SETTINGS } from '@Style/icons'; -import { StyleKit } from '@Style/StyleKit'; +import { StyleKit, StyleKitContext, ThemeContent } from '@Style/StyleKit'; +import _ from 'lodash'; import React, { Fragment, + useCallback, useContext, useEffect, useMemo, @@ -15,7 +17,7 @@ import { Platform } from 'react-native'; import FAB from 'react-native-fab'; import DrawerLayout from 'react-native-gesture-handler/DrawerLayout'; import Icon from 'react-native-vector-icons/Ionicons'; -import { ContentType, SNTag, SNTheme } from 'snjs'; +import { ContentType, SNTag, SNTheme, ThemeMutator } from 'snjs'; import { ThemeContext } from 'styled-components/native'; import { FirstSafeAreaView, @@ -33,6 +35,7 @@ type Props = { export const MainSideMenu = ({ drawerRef }: Props): JSX.Element => { // Context const theme = useContext(ThemeContext); + const styleKit = useContext(StyleKitContext); const application = useContext(ApplicationContext); const navigation = useNavigation(); @@ -54,7 +57,41 @@ export const MainSideMenu = ({ drawerRef }: Props): JSX.Element => { return removeTagChangeObserver; }); - const onThemeSelect = (_theme: SNTheme) => {}; + const onSystemThemeSelect = useCallback( + async (selectedTheme: ThemeContent) => { + const oldTheme = application!.findItem(styleKit!.activeThemeId!) as + | SNTheme + | undefined; + + styleKit?.activateSystemTheme(selectedTheme.uuid); + if (oldTheme?.isTheme() && oldTheme.isMobileActive()) { + await application?.changeAndSaveItem(oldTheme.uuid, mutator => { + const themeMutator = mutator as ThemeMutator; + themeMutator.setMobileActive(false); + }); + } + }, + [application, styleKit] + ); + + const onThemeSelect = useCallback( + async (selectedTheme: SNTheme) => { + if (!selectedTheme.isMobileActive()) { + await application?.changeItem(selectedTheme.uuid, mutator => { + const themeMutator = mutator as ThemeMutator; + themeMutator.setMobileActive(true); + }); + if (application!.findItem(styleKit!.activeThemeId!)) { + await application?.changeItem(styleKit!.activeThemeId!, mutator => { + const themeMutator = mutator as ThemeMutator; + themeMutator.setMobileActive(false); + }); + } + await application?.sync(); + } + }, + [application, styleKit] + ); useEffect(() => { const unsubscribeStreamThemes = application?.streamItems( @@ -68,18 +105,65 @@ export const MainSideMenu = ({ drawerRef }: Props): JSX.Element => { return unsubscribeStreamThemes; }, [application]); + const iconDescriptorForTheme = (currentTheme: SNTheme | ThemeContent) => { + const desc = { + type: 'circle', + side: 'right' as 'right', + }; + + const dockIcon = + currentTheme.package_info && currentTheme.package_info.dock_icon; + + if (dockIcon && dockIcon.type === 'circle') { + _.merge(desc, { + backgroundColor: dockIcon.background_color, + borderColor: dockIcon.border_color, + }); + } else { + _.merge(desc, { + backgroundColor: theme.stylekitInfoColor, + borderColor: theme.stylekitInfoColor, + }); + } + + return desc; + }; + const themeOptions = useMemo(() => { - let options: SideMenuOption[] = themes - .filter(el => !el.errorDecrypting) - .map(mapTheme => ({ - text: mapTheme.name, - key: mapTheme.uuid, - dimmed: mapTheme.getNotAvailOnMobile(), - onSelect: () => onThemeSelect(mapTheme), - })); + const options: SideMenuOption[] = styleKit! + .systemThemes() + .map(systemTheme => ({ + text: systemTheme?.name, + key: systemTheme?.uuid, + iconDesc: iconDescriptorForTheme(systemTheme), + dimmed: false, + onSelect: () => onSystemThemeSelect(systemTheme), + selected: styleKit!.activeThemeId === systemTheme?.uuid, + })) + .concat( + themes + .filter(el => !el.errorDecrypting) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(mapTheme => ({ + text: mapTheme.name, + key: mapTheme.uuid, + iconDesc: iconDescriptorForTheme(mapTheme), + dimmed: mapTheme.getNotAvailOnMobile(), + onSelect: () => onThemeSelect(mapTheme), + selected: styleKit!.activeThemeId === mapTheme.uuid, + })) + ); return options; - }, [themes]); + // We want to also track activeThemeId + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + styleKit, + styleKit?.activeThemeId, + themes, + onSystemThemeSelect, + onThemeSelect, + ]); const onTagSelect = async (tag: SNTag) => { if (tag.conflictOf) { diff --git a/src/screens/SideMenu/TagSelectionList.tsx b/src/screens/SideMenu/TagSelectionList.tsx index d2d43a59..c01dbcc8 100644 --- a/src/screens/SideMenu/TagSelectionList.tsx +++ b/src/screens/SideMenu/TagSelectionList.tsx @@ -34,20 +34,7 @@ export const TagSelectionList = (props: Props): JSX.Element => { if (props.contentType === ContentType.SmartTag) { setTags(application!.getSmartTags()); } else { - setTags( - (application!.getItems(props.contentType) as SNTag[]).sort((a, b) => { - if (!a.title) { - return -1; - } - if (!b.title) { - return 1; - } - if (!a.title && !b.title) { - return 0; - } - return a.title.localeCompare(b.title); - }) - ); + setTags(application!.getDisplayableItems(props.contentType) as SNTag[]); } }, [application, props.contentType]); diff --git a/src/style/StyleKit.ts b/src/style/StyleKit.ts index 31447677..e978e970 100644 --- a/src/style/StyleKit.ts +++ b/src/style/StyleKit.ts @@ -1,5 +1,5 @@ import { MobileApplication } from '@Lib/application'; -import ThemeDownloader from '@Style/Util/ThemeDownloader'; +import CSSParser from '@Style/Util/CSSParser'; import { DARK_CONTENT, keyboardColorForTheme, @@ -17,8 +17,9 @@ import { TextStyle, ViewStyle, } from 'react-native'; -import { ComponentArea, removeFromArray, SNComponent, SNTheme } from 'snjs'; +import { ContentType, removeFromArray, SNComponent, SNTheme } from 'snjs'; import { UuidString } from 'snjs/dist/@types/types'; +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 { StyleKitTheme } from './Themes/styled-components'; @@ -31,12 +32,14 @@ export interface ThemeContent { name: string; luminosity?: number; isSwapIn?: boolean; + uuid: string; variables: StyleKitTheme; package_info: SNComponent['package_info']; } enum SystemThemes { Blue = 'Blue', + Dark = 'Dark', Red = 'Red', } @@ -46,8 +49,8 @@ export const StyleKitContext = React.createContext( export class StyleKit { observers: ThemeChangeObserver[] = []; - private themeData: Partial> = {}; - activeTheme?: string; + private themeData: Record = {}; + activeThemeId?: string; currentDarkMode: ColorSchemeName; static constants = { mainTextFontSize: 16, @@ -74,28 +77,17 @@ export class StyleKit { this.unregisterComponentHandler && this.unregisterComponentHandler(); } - // onAppEvent(event: ApplicationEvent) { - // super.onAppEvent(event); - // if (event === ApplicationEvent.SignedOut) { - // this.resetToSystemTheme(); - // } - // } - private registerObservers() { - this.unregisterComponentHandler = this.application!.componentManager!.registerHandler( - { - identifier: 'themeManager', - areas: [ComponentArea.Themes], - activationHandler: (uuid, component) => { - if (component?.active) { - console.log('activeTheme'); - // this.activateTheme(component as SNTheme); - } else { - // this.deactivateTheme(uuid); - } - }, + this.application.streamItems(ContentType.Theme, items => { + const themes = items as SNTheme[]; + const activeTheme = themes.find(el => { + return !el.deleted && el.isMobileActive(); + }); + + if (activeTheme && activeTheme.uuid !== this.activeThemeId) { + this.activateExternalTheme(activeTheme); } - ); + }); } private findOrCreateDataForTheme(themeId: string) { @@ -110,11 +102,19 @@ export class StyleKit { private buildSystemThemesAndData() { const themeData = [ { + uuid: SystemThemes.Blue, variables: THEME_BLUE_JSON, name: SystemThemes.Blue, - isInitial: true, + isInitial: Appearance.getColorScheme() === 'light', }, { + uuid: SystemThemes.Dark, + variables: THEME_DARK_JSON, + name: SystemThemes.Dark, + isInitial: Appearance.getColorScheme() === 'dark', + }, + { + uuid: SystemThemes.Red, variables: THEME_RED_JSON, name: SystemThemes.Red, }, @@ -128,10 +128,11 @@ export class StyleKit { variables.statusBar = Platform.OS === 'android' ? LIGHT_CONTENT : DARK_CONTENT; - this.themeData[option.name] = { + this.themeData[option.uuid] = { isSystemTheme: true, isInitial: Boolean(option.isInitial), name: option.name, + uuid: option.uuid, variables: variables, package_info: { dock_icon: { @@ -174,7 +175,10 @@ export class StyleKit { * If we're changing the theme for a specific mode and we're currently on * that mode, then activate this theme. */ - if (this.currentDarkMode === currentMode && this.activeTheme !== themeId) { + if ( + this.currentDarkMode === currentMode && + this.activeThemeId !== themeId + ) { this.activateTheme(themeId); } } @@ -186,17 +190,17 @@ export class StyleKit { * copy as the result may be modified before use. */ templateVariables() { - return _.clone(THEME_RED_JSON); + return _.clone(THEME_BLUE_JSON); } private resetToSystemTheme() { this.activateTheme(Object.keys(this.themeData)[0]); } - async resolveInitialTheme() { + private async resolveInitialTheme() { // const currentMode = Appearance.getColorScheme(); const runDefaultTheme = () => { - const defaultTheme = SystemThemes.Blue; + const defaultTheme = SystemThemes.Dark; // TODO: save to starege @@ -231,17 +235,12 @@ export class StyleKit { keyboardColorForActiveTheme() { return keyboardColorForTheme( - this.findOrCreateDataForTheme(this.activeTheme!) + this.findOrCreateDataForTheme(this.activeThemeId!) ); } - themes() { - // TODO: add external themes - return this.themeData; - } - - isThemeActive(theme: SNTheme) { - return this.activeTheme && theme.uuid === this.activeTheme; + systemThemes() { + return Object.values(this.themeData).filter(th => th.isSystemTheme); } setActiveTheme(themeId: string) { @@ -250,7 +249,7 @@ export class StyleKit { const variables = themeData.variables; themeData.variables = _.merge(this.templateVariables(), variables); - this.activeTheme = themeId; + this.activeThemeId = themeId; this.updateDeviceForTheme(themeId); @@ -312,29 +311,61 @@ export class StyleKit { // } } - activateExternalTheme(theme: SNTheme) { - ThemeDownloader.get() - .downloadTheme(theme) - .then(variables => { - if (!variables) { - Alert.alert( - 'Not Available', - 'This theme is not available on mobile.' - ); - return; - } - // TODO: merge new theme style - // if (variables !== theme.content.variables) { - // theme.content.variables = variables; - // theme.setDirty(true); - // } - }); + private async downloadTheme(theme: SNTheme) { + let errorBlock = (error: null) => { + console.error('Theme download error', error); + }; + + let url = theme.hosted_url; + + if (!url) { + errorBlock(null); + return; + } + + if (Platform.OS === 'android' && url.includes('localhost')) { + url = url.replace('localhost', '10.0.2.2'); + } + + return new Promise(async resolve => { + try { + const response = await fetch(url, { + method: 'GET', + }); + // console.log(JSON.parse(response)); + const data = await response.text(); + // @ts-ignore TODO: check response type + let variables = CSSParser.cssToObject(data); + + resolve(variables); + } catch (e) { + resolve(null); + } + }); + } + + activateSystemTheme(themeId: string) { + this.activateTheme(themeId); + } + + async activateExternalTheme(theme: SNTheme) { + const variables = await this.downloadTheme(theme); + if (!variables) { + Alert.alert('Not Available', 'This theme is not available on mobile.'); + return; + } + let data = this.findOrCreateDataForTheme(theme.uuid); + const appliedVariables = _.merge(this.templateVariables(), variables); + data.variables = { + ...appliedVariables, + ...StyleKit.constants, + }; this.activateTheme(theme.uuid); } activateTheme(themeId: string) { this.setActiveTheme(themeId); - this.assignThemeForMode(themeId); + // this.assignThemeForMode(themeId); } // activatePreferredTheme() { @@ -359,7 +390,7 @@ export class StyleKit { // } async downloadThemeAndReload(theme: SNTheme) { - const updatedVariables = await ThemeDownloader.get().downloadTheme(theme); + const updatedVariables = await this.downloadTheme(theme); /** Merge default variables to ensure this theme has all the variables. */ const appliedVariables = _.merge( @@ -372,13 +403,13 @@ export class StyleKit { ...StyleKit.constants, }; - if (theme.uuid === this.activeTheme) { + if (theme.uuid === this.activeThemeId) { this.setActiveTheme(theme.uuid); } } reloadStyles() { - const { variables } = this.findOrCreateDataForTheme(this.activeTheme!); + const { variables } = this.findOrCreateDataForTheme(this.activeThemeId!); this.theme = variables; } diff --git a/src/style/Util/ThemeDownloader.ts b/src/style/Util/ThemeDownloader.ts deleted file mode 100644 index c8166c1b..00000000 --- a/src/style/Util/ThemeDownloader.ts +++ /dev/null @@ -1,44 +0,0 @@ -import CSSParser from '@Style/Util/CSSParser'; -import { Platform } from 'react-native'; -import { SNHttpService, SNTheme } from 'snjs'; - -export default class ThemeDownloader { - private static instance: ThemeDownloader; - private httpService = new SNHttpService(); - - static get() { - if (!this.instance) { - this.instance = new ThemeDownloader(); - } - - return this.instance; - } - - async downloadTheme(theme: SNTheme) { - let errorBlock = (error: null) => { - console.error('Theme download error', error); - }; - - let url = theme.hosted_url; - - if (!url) { - errorBlock(null); - return; - } - - if (Platform.OS === 'android' && url.includes('localhost')) { - url = url.replace('localhost', '10.0.2.2'); - } - - return new Promise(async resolve => { - try { - const response = await this.httpService.getAbsolute(url, {}); - // @ts-ignore TODO: check response type - let variables = CSSParser.cssToObject(response); - resolve(variables); - } catch (e) { - resolve(null); - } - }); - } -} diff --git a/yarn.lock b/yarn.lock index 9f132798..7e850dba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7023,9 +7023,9 @@ sncrypto@standardnotes/sncrypto#7e76ab9: version "1.1.3" resolved "https://codeload.github.com/standardnotes/sncrypto/tar.gz/7e76ab9977f85039d9399b935aecfe495a951edb" -snjs@standardnotes/snjs#84b0403: +snjs@standardnotes/snjs#27688ba: version "1.0.5" - resolved "https://codeload.github.com/standardnotes/snjs/tar.gz/84b04033e1877dbbe54a7b1e03065fa7a0044744" + resolved "https://codeload.github.com/standardnotes/snjs/tar.gz/27688ba55aa0d50b6bd8e27caf8d7b988fde411d" source-map-resolve@^0.5.0: version "0.5.3"