Files
mobile/src/Styles.js
Mo Bitar 40108faa28 Updates
2018-07-10 09:04:57 -05:00

584 lines
16 KiB
JavaScript

import { StyleSheet, StatusBar, Alert, Platform, Dimensions } from 'react-native';
import App from "./app"
import ModelManager from "./lib/sfjs/modelManager"
import Server from "./lib/sfjs/httpManager"
import Sync from './lib/sfjs/syncManager'
import Storage from "./lib/sfjs/storageManager"
import Auth from "./lib/sfjs/authManager"
import KeysManager from './lib/keysManager'
export default class GlobalStyles {
static instance = null;
static get() {
if (this.instance == null) {
this.instance = new GlobalStyles();
}
return this.instance;
}
async resolveInitialTheme() {
// Get the active theme from storage rather than waiting for local database to load
var themeResult = await Storage.get().getItem("activeTheme");
let runDefaultTheme = () => {
try {
var theme = this.systemTheme();
theme.setMobileActive(true);
this.activeTheme = theme;
var constants = this.defaultConstants();
this.setStyles(this.defaultRules(constants), constants, theme.getMobileRules().statusBar);
} catch (e) {
var constants = this.defaultConstants();
this.setStyles(this.defaultRules(constants), constants, Platform.OS == "android" ? "light-content" : "dark-content");
console.log("Default theme error", e);
}
}
console.log("Theme result", themeResult);
if(themeResult) {
// JSON stringified content is generic and includes all items property at time of stringification
// So we parse it, then set content to itself, so that the mapping can be handled correctly.
try {
var parsedTheme = JSON.parse(themeResult);
var needsMigration = false;
if(parsedTheme.mobileRules) {
// Newer versions of the app persist a Theme object where mobileRules are nested in AppData.
// We want to check if the currently saved data is of the old format, which uses theme.mobileRules
// instead of theme.getMobileRules(). If so, we want to prepare it for the new format.
needsMigration = true;
}
let content = Object.assign({}, parsedTheme);
parsedTheme.content = content;
var theme = new SNTheme(parsedTheme);
if(needsMigration) {
theme.setMobileRules(parsedTheme.mobileRules);
theme.mobileRules = null;
}
theme.isSwapIn = true;
var constants = _.merge(this.defaultConstants(), theme.getMobileRules().constants);
var rules = _.merge(this.defaultRules(constants), theme.getMobileRules().rules);
this.setStyles(rules, constants, theme.getMobileRules().statusBar);
this.activeTheme = theme;
} catch (e) {
console.error("Error parsing initial theme", e);
runDefaultTheme();
}
} else {
runDefaultTheme();
}
}
constructor() {
KeysManager.get().registerAccountRelatedStorageKeys(["activeTheme"]);
ModelManager.get().addItemSyncObserver("themes", "SN|Theme", function(allItems, validItems, deletedItems, source){
if(this.activeTheme && this.activeTheme.isSwapIn) {
var matchingTheme = _.find(this.themes(), {uuid: this.activeTheme.uuid});
if(matchingTheme) {
this.activeTheme = matchingTheme;
this.activeTheme.isSwapIn = false;
this.activeTheme.setMobileActive(true);
}
}
}.bind(this));
}
static styles() {
return this.get().styles.rules;
}
static constants() {
return this.get().styles.constants;
}
static stylesForKey(key) {
var rules = this.get().styles.rules;
var styles = [rules[key]];
var platform = Platform.OS == "android" ? "Android" : "IOS";
var platformRules = rules[key+platform];
if(platformRules) {
styles.push(platformRules);
}
return styles;
}
static constantForKey(key) {
var defaultValue = this.get().constants[key];
try {
// For the platform value, if the active theme does not have a specific value, but the defaults do, we don't
// want to use the defaults, but instead just look at the activeTheme. Because default platform values only apply
// to the default theme
var platform = Platform.OS == "android" ? "Android" : "IOS";
var platformValue = this.get().activeTheme.getMobileRules().constants[key+platform];
if(platformValue) {
return platformValue;
} else {
return defaultValue;
}
} catch (e) {
return defaultValue;
}
}
systemTheme() {
if(this._systemTheme) {
return this._systemTheme;
}
var constants = this.defaultConstants();
this._systemTheme = new SNTheme({
uuid: 0,
content: {
isDefault: true,
name: "Default",
}
});
this._systemTheme.setMobileRules({
name: "Default",
rules: this.defaultRules(constants),
constants: constants,
statusBar: Platform.OS == "android" ? "light-content" : "dark-content"
})
return this._systemTheme;
}
themes() {
return [this.systemTheme()].concat(ModelManager.get().themes);
}
isThemeActive(theme) {
if(this.activeTheme) {
return theme.uuid == this.activeTheme.uuid;
}
return theme.isMobileActive();
}
activateTheme(theme, writeToStorage = true) {
if(this.activeTheme) {
this.activeTheme.setMobileActive(false);
}
var run = () => {
var constants = _.merge(this.defaultConstants(), theme.getMobileRules().constants);
var rules = _.merge(this.defaultRules(constants), theme.getMobileRules().rules);
this.setStyles(rules, constants, theme.getMobileRules().statusBar);
this.activeTheme = theme;
theme.setMobileActive(true);
if(theme.content.isDefault) {
Storage.get().removeItem("activeTheme");
} else if(writeToStorage) {
Storage.get().setItem("activeTheme", JSON.stringify(theme));
}
App.get().reload();
}
if(!theme.hasMobileRules()) {
this.downloadTheme(theme, function(){
if(theme.getNotAvailOnMobile()) {
Alert.alert("Not Available", "This theme is not available on mobile.");
} else {
Sync.get().sync();
run();
}
});
} else {
run();
}
}
async downloadTheme(theme, callback) {
let errorBlock = (error) => {
if(!theme.getNotAvailOnMobile()) {
theme.setNotAvailOnMobile(true);
theme.setDirty(true);
}
callback && callback();
console.error("Theme download error", error);
}
var url = theme.hosted_url || theme.url;
if(!url) {
errorBlock(null);
return;
}
if(url.includes("?")) {
url = url.replace("?", ".json?");
} else if(url.includes(".css?")) {
url = url.replace(".css?", ".json?");
} else {
url = url + ".json";
}
if(App.isAndroid && url.includes("localhost")) {
url = url.replace("localhost", "10.0.2.2");
}
return Server.get().getAbsolute(url, {}, function(response){
// success
if(response !== theme.getMobileRules()) {
theme.setMobileRules(response);
theme.setDirty(true);
}
if(theme.getNotAvailOnMobile()) {
theme.setNotAvailOnMobile(false);
theme.setDirty(true);
}
if(callback) {
callback();
}
}, function(response) {
errorBlock(response);
})
}
downloadThemeAndReload(theme) {
this.downloadTheme(theme, function(){
Sync.get().sync(function(){
this.activateTheme(theme);
}.bind(this));
}.bind(this))
}
setStyles(rules, constants, statusBar) {
if(!statusBar) { statusBar = "light-content";}
this.statusBar = statusBar;
this.constants = constants;
this.styles = {
rules: StyleSheet.create(rules),
constants: constants
}
// On Android, a time out is required, especially during app startup
setTimeout(function () {
StatusBar.setBarStyle(statusBar, true);
}, Platform.OS == "android" ? 100 : 0);
}
static isIPhoneX() {
// See https://mydevice.io/devices/ for device dimensions
const X_WIDTH = 375;
const X_HEIGHT = 812;
const { height: D_HEIGHT, width: D_WIDTH } = Dimensions.get('window');
return Platform.OS === 'ios' &&
((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) ||
(D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT));
}
defaultConstants() {
var tintColor = "#fb0206";
return {
composeBorderColor: "#F5F5F5",
mainBackgroundColor: "#ffffff",
mainTintColor: tintColor,
mainDimColor: "#9d9d9d",
mainTextColor: "#000000",
mainTextFontSize: 16,
mainHeaderFontSize: 16,
navBarColor: "white",
navBarTextColor: tintColor,
navBarColorAndroid: tintColor,
navBarTextColorAndroid: "#ffffff",
paddingLeft: 14,
plainCellBorderColor: "#efefef",
sectionedCellHorizontalPadding: 14,
selectedBackgroundColor: "#efefef",
maxSettingsCellHeight: 45
}
}
defaultRules(constants) {
return {
container: {
backgroundColor: constants.mainBackgroundColor,
height: "100%",
},
flexContainer: {
flex: 1,
flexDirection: 'column',
backgroundColor: constants.mainBackgroundColor,
},
centeredContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
},
flexedItem: {
flexGrow: 1
},
uiText: {
color: constants.mainTextColor,
fontSize: constants.mainTextFontSize,
},
view: {
backgroundColor: constants.mainBackgroundColor,
},
tableSection: {
marginTop: 10,
marginBottom: 10,
backgroundColor: constants.mainBackgroundColor
},
sectionHeaderContainer: {
flex: 1,
flexGrow: 0,
justifyContent: "space-between",
flexDirection: 'row',
paddingRight: constants.paddingLeft,
paddingBottom: 10,
paddingTop: 10,
},
sectionHeader: {
fontSize: constants.mainTextFontSize - 4,
paddingLeft: constants.paddingLeft,
color: constants.mainDimColor,
fontWeight: Platform.OS == "android" ? "bold" : "normal"
},
sectionHeaderButton: {
color: constants.mainTintColor
},
sectionHeaderAndroid: {
fontSize: constants.mainTextFontSize - 2,
color: constants.mainTintColor
},
sectionedTableCell: {
borderBottomColor: constants.plainCellBorderColor,
borderBottomWidth: 1,
paddingLeft: constants.paddingLeft,
paddingRight: constants.paddingLeft,
paddingTop: 13,
paddingBottom: 12,
backgroundColor: constants.mainBackgroundColor,
flex: 1,
},
textInputCell: {
maxHeight: 50,
paddingTop: 0,
paddingBottom: 0
},
sectionedTableCellTextInput: {
fontSize: constants.mainTextFontSize,
padding: 0,
color: constants.mainTextColor,
height: "100%"
},
sectionedTableCellFirst: {
borderTopColor: constants.plainCellBorderColor,
borderTopWidth: 1,
},
sectionedTableCellLast: {
},
sectionedTableCellFirstAndroid: {
borderTopWidth: 0,
},
sectionedTableCellLastAndroid: {
borderBottomWidth: 0,
borderTopWidth: 0,
},
sectionedAccessoryTableCell: {
paddingTop: 0,
paddingBottom: 0,
minHeight: 47,
},
sectionedAccessoryTableCellLabel: {
fontSize: constants.mainTextFontSize,
color: constants.mainTextColor,
minWidth: "80%"
},
buttonCell: {
paddingTop: 0,
paddingBottom: 0,
flex: 1,
justifyContent: 'center'
},
buttonCellButton: {
textAlign: "center",
textAlignVertical: "center",
color: Platform.OS == "android" ? constants.mainTextColor : constants.mainTintColor,
fontSize: constants.mainTextFontSize,
},
buttonCellButtonLeft: {
textAlign: "left",
},
noteText: {
flexGrow: 1,
marginTop: 0,
paddingTop: 10,
color: constants.mainTextColor,
paddingLeft: constants.paddingLeft,
paddingRight: constants.paddingLeft,
paddingBottom: 10,
},
noteTextIOS: {
paddingLeft: constants.paddingLeft - 5,
paddingRight: constants.paddingLeft - 5,
},
syncBar: {
position: "absolute",
bottom: 0,
width: "100%",
backgroundColor: constants.mainTextColor,
padding: 5
},
syncBarText: {
textAlign: "center",
color: constants.mainBackgroundColor
},
actionSheetWrapper: {
},
actionSheetOverlay: {
// This is the dimmed background
// backgroundColor: constants.mainDimColor
},
actionSheetBody: {
// This will also set button border bottoms, since margin is used instead of borders
backgroundColor: constants.plainCellBorderColor
},
actionSheetTitleWrapper: {
backgroundColor: constants.mainBackgroundColor,
marginBottom: 1
},
actionSheetTitleText: {
color: constants.mainTextColor,
opacity: 0.5
},
actionSheetButtonWrapper: {
backgroundColor: constants.mainBackgroundColor,
marginTop: 0
},
actionSheetButtonTitle: {
color: constants.mainTextColor,
},
actionSheetCancelButtonWrapper: {
marginTop: 0
},
actionSheetCancelButtonTitle: {
color: constants.mainTintColor,
fontWeight: "normal"
},
bold: {
fontWeight: "bold"
},
}
}
static actionSheetStyles() {
return {
wrapperStyle: GlobalStyles.styles().actionSheetWrapper,
overlayStyle: GlobalStyles.styles().actionSheetOverlay,
bodyStyle : GlobalStyles.styles().actionSheetBody,
buttonWrapperStyle: GlobalStyles.styles().actionSheetButtonWrapper,
buttonTitleStyle: GlobalStyles.styles().actionSheetButtonTitle,
titleWrapperStyle: GlobalStyles.styles().actionSheetTitleWrapper,
titleTextStyle: GlobalStyles.styles().actionSheetTitleText,
tintColor: App.isIOS ? undefined : GlobalStyles.constants().mainTintColor,
buttonUnderlayColor: GlobalStyles.constants().plainCellBorderColor,
cancelButtonWrapperStyle: GlobalStyles.styles().actionSheetCancelButtonWrapper,
cancelButtonTitleStyle: GlobalStyles.styles().actionSheetCancelButtonTitle,
cancelMargin: StyleSheet.hairlineWidth
}
}
static shadeBlend(p,c0,c1) {
var n=p<0?p*-1:p,u=Math.round,w=parseInt;
if(c0.length>7){
var 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{
var 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)
}
}
static darken(color, value = -0.15) {
return this.shadeBlend(value, color);
}
static lighten(color, value = 0.25) {
return this.shadeBlend(value, color);
}
static hexToRGBA(hex, alpha) {
if(!hex) {
return null;
}
var c;
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');
}
}
}