feature: WIP themes

This commit is contained in:
Radek Czemerys
2020-08-19 22:11:22 +02:00
parent 9d9bbc51f3
commit dfcab67264
8 changed files with 222 additions and 142 deletions

View File

@@ -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"
},

View File

@@ -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<StyleKit | undefined>(undefined);
const styleKit = useRef<StyleKit>();
const [activeTheme, setActiveTheme] = useState<StyleKitTheme | undefined>();
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 (
<NavigationContainer
theme={{
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: styleKit.current.theme!.stylekitBackgroundColor,
border: styleKit.current.theme!.stylekitBorderColor,
background: activeTheme.stylekitBackgroundColor,
border: activeTheme.stylekitBorderColor,
},
}}
onReady={() => {
@@ -459,7 +480,7 @@ const AppComponent: React.FC<{
<StatusBar translucent />
{styleKit.current && (
<>
<ThemeProvider theme={styleKit.current.theme!}>
<ThemeProvider theme={activeTheme}>
<ActionSheetProvider>
<StyleKitContext.Provider value={styleKit.current}>
<MainStackComponent env={env} />

View File

@@ -65,6 +65,7 @@ export const CompanySection = (props: Props) => {
<SectionHeader title={props.title} />
<ButtonCell
first
leftAligned={true}
title="Help"
onPress={() => openUrl('help')}

View File

@@ -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) {

View File

@@ -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]);

View File

@@ -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<StyleKit | undefined>(
export class StyleKit {
observers: ThemeChangeObserver[] = [];
private themeData: Partial<Record<UuidString, ThemeContent>> = {};
activeTheme?: string;
private themeData: Record<UuidString, ThemeContent> = {};
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;
}

View File

@@ -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);
}
});
}
}

View File

@@ -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"