diff --git a/src/Styles.js b/src/Styles.js index 11330aab..3ebeb1f7 100644 --- a/src/Styles.js +++ b/src/Styles.js @@ -26,19 +26,23 @@ export default class GlobalStyles { // Get the active theme from storage rather than waiting for local database to load return Storage.getItem("activeTheme").then(function(theme) { if(theme) { + // 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. theme = JSON.parse(theme); + theme.content = theme; + theme = new Theme(theme); theme.isSwapIn = true; - var constants = _.merge(this.defaultConstants(), theme.mobileRules.constants); - var rules = _.merge(this.defaultRules(constants), theme.mobileRules.rules); - this.setStyles(rules, constants, theme.mobileRules.statusBar); + 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; } else { var theme = this.systemTheme(); - theme.active = true; + theme.setMobileActive(true); this.activeTheme = theme; var constants = this.defaultConstants(); - this.setStyles(this.defaultRules(constants), constants, theme.mobileRules.statusBar); + this.setStyles(this.defaultRules(constants), constants, theme.getMobileRules().statusBar); } }.bind(this)); } @@ -50,7 +54,7 @@ export default class GlobalStyles { if(this.activeTheme && this.activeTheme.isSwapIn) { this.activeTheme.isSwapIn = false; this.activeTheme = _.find(this.themes(), {uuid: this.activeTheme.uuid}); - this.activeTheme.active = true; + this.activeTheme.setMobileActive(true); } }.bind(this)); } @@ -81,11 +85,11 @@ export default class GlobalStyles { // 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"; - if(!this.get().activeTheme.mobileRules) { + if(!this.get().activeTheme.hasMobileRules()) { return null; } - var platformValue = this.get().activeTheme.mobileRules.constants[key+platform]; + var platformValue = this.get().activeTheme.getMobileRules().constants[key+platform]; if(platformValue) { return platformValue; @@ -102,14 +106,14 @@ export default class GlobalStyles { this._systemTheme = new Theme({ name: "Default", default: true, - uuid: 0, - mobileRules: { - name: "Default", - rules: this.defaultRules(constants), - constants: constants, - statusBar: Platform.OS == "android" ? "light-content" : "dark-content" - } + uuid: 0 }); + this._systemTheme.setMobileRules({ + name: "Default", + rules: this.defaultRules(constants), + constants: constants, + statusBar: Platform.OS == "android" ? "light-content" : "dark-content" + }) return this._systemTheme; } @@ -123,16 +127,18 @@ export default class GlobalStyles { activateTheme(theme, writeToStorage = true) { if(this.activeTheme) { - this.activeTheme.active = false; + this.activeTheme.setMobileActive(false); + this.activeTheme.setDirty(true); } var run = () => { - var constants = _.merge(this.defaultConstants(), theme.mobileRules.constants); - var rules = _.merge(this.defaultRules(constants), theme.mobileRules.rules); - this.setStyles(rules, constants, theme.mobileRules.statusBar); + 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.active = true; + theme.setMobileActive(true); + theme.setDirty(true); if(theme.default) { Storage.removeItem("activeTheme"); @@ -143,11 +149,12 @@ export default class GlobalStyles { App.get().reload(); } - if(!theme.mobileRules) { + if(!theme.hasMobileRules()) { this.downloadTheme(theme, function(){ - if(theme.notAvailableOnMobile) { + if(theme.getNotAvailOnMobile()) { Alert.alert("Not Available", "This theme is not available on mobile."); } else { + Sync.getInstance().sync(); run(); } }); @@ -158,8 +165,8 @@ export default class GlobalStyles { async downloadTheme(theme, callback) { let errorBlock = (error) => { - if(!theme.notAvailableOnMobile) { - theme.notAvailableOnMobile = true; + if(!theme.getNotAvailOnMobile()) { + theme.setNotAvailOnMobile(true); theme.setDirty(true); } @@ -168,17 +175,17 @@ export default class GlobalStyles { console.error("Theme download error", error); } - if(!theme.url) { + var url = theme.url; + + if(!url) { errorBlock(null); return; } - var url; - - if(theme.url.includes("?")) { - url = theme.url.replace("?", ".json?"); + if(url.includes("?")) { + url = url.replace("?", ".json?"); } else { - url = theme.url + ".json"; + url = url + ".json"; } if(App.isAndroid && url.includes("localhost")) { @@ -187,13 +194,13 @@ export default class GlobalStyles { return Server.getInstance().getAbsolute(url, {}, function(response){ // success - if(response !== theme.mobileRules) { - theme.mobileRules = response; + if(response !== theme.getMobileRules()) { + theme.setMobileRules(response); theme.setDirty(true); } - if(theme.notAvailableOnMobile) { - theme.notAvailableOnMobile = false; + if(theme.getNotAvailOnMobile()) { + theme.setNotAvailOnMobile(false); theme.setDirty(true); } diff --git a/src/containers/account/AuthSection.js b/src/containers/account/AuthSection.js index 2d886d95..a32e97ea 100644 --- a/src/containers/account/AuthSection.js +++ b/src/containers/account/AuthSection.js @@ -98,6 +98,7 @@ export default class AuthSection extends AbstractComponent { autoFocus={true} underlineColorAndroid={'transparent'} placeholderTextColor={GlobalStyles.constants().mainDimColor} + onSubmitEditing={this.onSignInPress} /> diff --git a/src/containers/account/ThemesSection.js b/src/containers/account/ThemesSection.js index fa1c374a..0fa85a4d 100644 --- a/src/containers/account/ThemesSection.js +++ b/src/containers/account/ThemesSection.js @@ -23,8 +23,8 @@ export default class ThemesSection extends Component { text={theme.name} key={theme.uuid} first={i == 0} - selected={() => {return theme.active}} - dimmed={theme.notAvailableOnMobile} + selected={() => {return theme.isMobileActive()}} + dimmed={theme.getNotAvailOnMobile()} last={i == this.props.themes.length - 1 && this.props.themes.length > 1} /> ) diff --git a/src/models/app/component.js b/src/models/app/component.js new file mode 100644 index 00000000..1fed24d0 --- /dev/null +++ b/src/models/app/component.js @@ -0,0 +1,126 @@ +import Item from "../api/item" +var _ = require('lodash') + +export default class Component extends Item { + + constructor(json_obj) { + super(json_obj); + + if(!this.componentData) { + this.componentData = {}; + } + + if(!this.disassociatedItemIds) { + this.disassociatedItemIds = []; + } + + if(!this.associatedItemIds) { + this.associatedItemIds = []; + } + } + + mapContentToLocalProperties(content) { + super.mapContentToLocalProperties(content) + /* Legacy */ + this.url = content.url; + /* New */ + this.local_url = content.local_url; + this.hosted_url = content.hosted_url; + this.offlineOnly = content.offlineOnly; + + if(content.valid_until) { + this.valid_until = new Date(content.valid_until); + } + + this.name = content.name; + this.autoupdateDisabled = content.autoupdateDisabled; + + this.package_info = content.package_info; + + // the location in the view this component is located in. Valid values are currently tags-list, note-tags, and editor-stack` + this.area = content.area; + + this.permissions = content.permissions; + this.active = content.active; + + // custom data that a component can store in itself + this.componentData = content.componentData || {}; + + // items that have requested a component to be disabled in its context + this.disassociatedItemIds = content.disassociatedItemIds || []; + + // items that have requested a component to be enabled in its context + this.associatedItemIds = content.associatedItemIds || []; + } + + structureParams() { + var params = { + url: this.url, + hosted_url: this.hosted_url, + local_url: this.local_url, + valid_until: this.valid_until, + offlineOnly: this.offlineOnly, + name: this.name, + area: this.area, + package_info: this.package_info, + permissions: this.permissions, + active: this.active, + autoupdateDisabled: this.autoupdateDisabled, + componentData: this.componentData, + disassociatedItemIds: this.disassociatedItemIds, + associatedItemIds: this.associatedItemIds, + }; + + _.merge(params, super.structureParams()); + return params; + } + + get content_type() { + return "SN|Component"; + } + + isEditor() { + return this.area == "editor-editor"; + } + + isDefaultEditor() { + return this.getAppDataItem("defaultEditor") == true; + } + + setLastSize(size) { + this.setAppDataItem("lastSize", size); + } + + getLastSize() { + return this.getAppDataItem("lastSize"); + } + + keysToIgnoreWhenCheckingContentEquality() { + return ["active"].concat(super.keysToIgnoreWhenCheckingContentEquality()); + } + + + /* + An associative component depends on being explicitly activated for a given item, compared to a dissaciative component, + which is enabled by default in areas unrelated to a certain item. + */ + static associativeAreas() { + return ["editor-editor"]; + } + + isAssociative() { + return Component.associativeAreas().includes(this.area); + } + + associateWithItem(item) { + this.associatedItemIds.push(item.uuid); + } + + isExplicitlyEnabledForItem(item) { + return this.associatedItemIds.indexOf(item.uuid) !== -1; + } + + isExplicitlyDisabledForItem(item) { + return this.disassociatedItemIds.indexOf(item.uuid) !== -1; + } +} diff --git a/src/models/app/theme.js b/src/models/app/theme.js index 2c5cae18..12a31e1e 100644 --- a/src/models/app/theme.js +++ b/src/models/app/theme.js @@ -1,34 +1,12 @@ -import Item from "../api/item" +import Component from "./component" var _ = require('lodash') -export default class Theme extends Item { +export default class Theme extends Component { constructor(json_obj) { super(json_obj); } - mapContentToLocalProperties(contentObject) { - super.mapContentToLocalProperties(contentObject) - this.url = contentObject.url; - this.hosted_url = contentObject.hosted_url; - this.name = contentObject.name; - this.mobileRules = contentObject.mobileRules; - this.notAvailableOnMobile = contentObject.notAvailableOnMobile; - } - - structureParams() { - var params = { - url: this.url, - name: this.name, - hosted_url: this.hosted_url, - mobileRules: this.mobileRules, - notAvailableOnMobile: this.notAvailableOnMobile - }; - - _.merge(params, super.structureParams()); - return params; - } - get content_type() { return "SN|Theme"; } @@ -36,4 +14,33 @@ export default class Theme extends Item { get displayName() { return "Theme"; } + + setMobileRules(rules) { + this.setAppDataItem("mobileRules", rules); + } + + getMobileRules() { + return this.getAppDataItem("mobileRules") || {constants: {}, rules: {}}; + } + + // Same as getMobileRules but without default value + hasMobileRules() { + return this.getAppDataItem("mobileRules"); + } + + setNotAvailOnMobile(na) { + this.setAppDataItem("notAvailableOnMobile", na); + } + + getNotAvailOnMobile() { + return this.getAppDataItem("notAvailableOnMobile"); + } + + setMobileActive(active) { + this.setAppDataItem("mobileActive", active); + } + + isMobileActive() { + return this.getAppDataItem("mobileActive"); + } }