mirror of
https://github.com/standardnotes/mobile.git
synced 2026-04-19 21:58:51 -04:00
584 lines
16 KiB
JavaScript
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');
|
|
}
|
|
}
|
|
|
|
}
|