diff --git a/app/assets/javascripts/app/frontend/controllers/_base.js b/app/assets/javascripts/app/frontend/controllers/_base.js index a220696a4..74d1bf18b 100644 --- a/app/assets/javascripts/app/frontend/controllers/_base.js +++ b/app/assets/javascripts/app/frontend/controllers/_base.js @@ -1,11 +1,12 @@ class BaseCtrl { - constructor(syncManager, dbManager) { + constructor($scope, syncManager, dbManager, $timeout) { dbManager.openDatabase(null, function(){ // new database, delete syncToken so that items can be refetched entirely from server syncManager.clearSyncToken(); syncManager.sync(); }) } + } angular.module('app.frontend').controller('BaseCtrl', BaseCtrl); diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index 0a87f0165..8f0f6a4ab 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -1,5 +1,5 @@ angular.module('app.frontend') -.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, syncManager, authManager) { +.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, syncManager, authManager, themeManager) { function urlParam(key) { return $location.search()[key]; @@ -34,6 +34,7 @@ angular.module('app.frontend') syncManager.loadLocalItems(function(items) { $scope.allTag.didLoad = true; + themeManager.activateInitialTheme(); $scope.$apply(); syncManager.sync(null); diff --git a/app/assets/javascripts/app/frontend/models/app/theme.js b/app/assets/javascripts/app/frontend/models/app/theme.js new file mode 100644 index 000000000..91b3e4915 --- /dev/null +++ b/app/assets/javascripts/app/frontend/models/app/theme.js @@ -0,0 +1,30 @@ +class Theme extends Item { + + constructor(json_obj) { + super(json_obj); + } + + mapContentToLocalProperties(contentObject) { + super.mapContentToLocalProperties(contentObject) + this.url = contentObject.url; + this.name = contentObject.name; + } + + structureParams() { + var params = { + url: this.url, + name: this.name, + }; + + _.merge(params, super.structureParams()); + return params; + } + + toJSON() { + return {uuid: this.uuid} + } + + get content_type() { + return "SN|Theme"; + } +} diff --git a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js index 8365a67ab..a5d7e0a90 100644 --- a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js @@ -7,10 +7,12 @@ class GlobalExtensionsMenu { }; } - controller($scope, extensionManager, syncManager) { + controller($scope, extensionManager, syncManager, modelManager, themeManager) { 'ngInject'; $scope.extensionManager = extensionManager; + $scope.themeManager = themeManager; + $scope.state = {showDataExts: true, showThemes: true}; $scope.toggleExtensionForm = function() { $scope.newExtensionData = {}; @@ -64,6 +66,19 @@ class GlobalExtensionsMenu { extensionManager.refreshExtensionsFromServer(); } } + + $scope.submitTheme = function() { + themeManager.submitTheme($scope.state.themeUrl); + } + + $scope.deleteTheme = function(theme) { + if(confirm("Are you sure you want to delete this theme?")) { + themeManager.deactivateTheme(theme); + modelManager.setItemToBeDeleted(theme); + syncManager.sync(); + } + } + } } diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index f5b5c04ac..128f2a2ac 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -8,7 +8,7 @@ class ModelManager { this.itemChangeObservers = []; this.items = []; this._extensions = []; - this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor"]; + this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme"]; } get allItems() { @@ -130,7 +130,9 @@ class ModelManager { item = new Extension(json_obj); } else if(json_obj.content_type == "SN|Editor") { item = new Editor(json_obj); - } else { + } else if(json_obj.content_type == "SN|Theme") { + item = new Theme(json_obj); + } else { item = new Item(json_obj); } @@ -255,7 +257,6 @@ class ModelManager { _.pull(this.tags, item); } else if(item.content_type == "Note") { _.pull(this.notes, item); - } else if(item.content_type == "Extension") { _.pull(this._extensions, item); } diff --git a/app/assets/javascripts/app/services/themeManager.js b/app/assets/javascripts/app/services/themeManager.js new file mode 100644 index 000000000..f619dabd5 --- /dev/null +++ b/app/assets/javascripts/app/services/themeManager.js @@ -0,0 +1,82 @@ +class ThemeManager { + + constructor(modelManager, syncManager) { + this.syncManager = syncManager; + this.modelManager = modelManager; + } + + get themes() { + return this.modelManager.itemsForContentType("SN|Theme"); + } + + get activeTheme() { + var activeThemeId = localStorage.getItem("activeTheme"); + if(!activeThemeId) { + return null; + } + + var theme = _.find(this.themes, {uuid: activeThemeId}); + return theme; + } + + activateInitialTheme() { + var theme = this.activeTheme; + if(theme) { + this.activateTheme(theme); + } + } + + submitTheme(url) { + var name = this.displayNameForThemeFile(this.fileNameFromPath(url)); + var theme = this.modelManager.createItem({content_type: "SN|Theme", url: url, name: name}); + this.modelManager.addItem(theme); + theme.setDirty(true); + this.syncManager.sync(); + } + + activateTheme(theme) { + var activeTheme = this.activeTheme; + if(activeTheme) { + this.deactivateTheme(activeTheme); + } + + var link = document.createElement("link"); + link.href = theme.url; + link.type = "text/css"; + link.rel = "stylesheet"; + link.media = "screen,print"; + link.id = theme.uuid; + document.getElementsByTagName("head")[0].appendChild(link); + localStorage.setItem("activeTheme", theme.uuid); + } + + deactivateTheme(theme) { + localStorage.removeItem("activeTheme"); + var element = document.getElementById(theme.uuid); + if(element) { + element.disabled = true; + element.parentNode.removeChild(element); + } + } + + isThemeActive(theme) { + return localStorage.getItem("activeTheme") === theme.uuid; + } + + fileNameFromPath(filePath) { + return filePath.replace(/^.*[\\\/]/, ''); + } + + capitalizeString(string) { + return string.replace(/(?:^|\s)\S/g, function(a) { return a.toUpperCase(); }); + } + + displayNameForThemeFile(fileName) { + let name = fileName.split(".")[0]; + let cleaned = name.split("-").join(" "); + return this.capitalizeString(cleaned); + } + +} + +angular.module('app.frontend').service('themeManager', ThemeManager); diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 30a98150a..b518e2527 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -56,6 +56,9 @@ h2 { display: block; } + h1 { + font-size: 16px; + } h3 { font-size: 14px ; @@ -164,6 +167,40 @@ a.disabled { pointer-events: none; } + + + + + + +/* Global Ext Menu */ + +.ext-section { + + min-height: 50px; + + h1 { + margin: 0; + padding: 0; + padding-top: 4px; + } + + &.opened { + h1 { + padding-top: 0px; + padding-bottom: 6px; + } + } + +} + + + + + + + + .spinner { height: 10px; width: 10px; diff --git a/app/assets/stylesheets/app/_standard.scss b/app/assets/stylesheets/app/_standard.scss index 60cc3601a..80734ae68 100644 --- a/app/assets/stylesheets/app/_standard.scss +++ b/app/assets/stylesheets/app/_standard.scss @@ -66,6 +66,10 @@ margin-right: 20px; } +.ml-2 { + margin-left: 2px; +} + .pb-0 { padding-bottom: 0px !important; } diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index a28067719..80d4b0df5 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -1,57 +1,79 @@ .panel.panel-default.account-panel.panel-right .panel-body - %div{"style" => "font-size: 15px;", "ng-if" => "!extensionManager.extensions.length"} No extensions installed - %div{"ng-if" => "extensionManager.extensions.length"} - %section.gray-bg.inline-h.mb-10.medium-padding{"ng-repeat" => "extension in extensionManager.extensions | orderBy: 'name'", "ng-init" => "extension.formData = {}"} - %h3.center-align {{extension.name}} - .center-align.centered.mt-10 - %label.block.normal Send data: - %label.normal - %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"} - Encrypted - %label.normal - %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"} - Decrypted + .white-bg.medium-padding.ext-section{"ng-class" => "{'opened': state.showDataExts}"} + %h1 + %a{"ng-click" => "state.showDataExts = !state.showDataExts"} Data Extensions + %div{"ng-if" => "state.showDataExts"} + %div{"style" => "font-size: 15px;", "ng-if" => "!extensionManager.extensions.length"} No extensions installed + %div{"ng-if" => "extensionManager.extensions.length"} + %section.gray-bg.inline-h.mb-10.medium-padding{"ng-repeat" => "extension in extensionManager.extensions | orderBy: 'name'", "ng-init" => "extension.formData = {}"} + %h3.center-align {{extension.name}} + .center-align.centered.mt-10 + %label.block.normal Send data: + %label.normal + %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"} + Encrypted + %label.normal + %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"} + Decrypted - .small-v-space + .small-v-space - %section.inline-h.white-bg.medium-padding.mb-10.pb-4{"ng-repeat" => "action in extension.actionsInGlobalContext()"} - %label.block {{action.label}} - %em{"style" => "font-style: italic;"} {{action.desc}} - %em{"ng-if" => "action.repeat_mode == 'watch'"} - Repeats when a change is made to your items. - %em{"ng-if" => "action.repeat_mode == 'loop'"} - Repeats at most once every {{action.repeat_timeout}} seconds - %div - %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} - %div{"ng-if" => "action.showPermissions"} - {{action.permissionsString()}} - %label.block.normal {{action.encryptionModeString()}} + %section.inline-h.white-bg.medium-padding.mb-10.pb-4{"ng-repeat" => "action in extension.actionsInGlobalContext()"} + %label.block {{action.label}} + %em{"style" => "font-style: italic;"} {{action.desc}} + %em{"ng-if" => "action.repeat_mode == 'watch'"} + Repeats when a change is made to your items. + %em{"ng-if" => "action.repeat_mode == 'loop'"} + Repeats at most once every {{action.repeat_timeout}} seconds + %div + %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} + %div{"ng-if" => "action.showPermissions"} + {{action.permissionsString()}} + %label.block.normal {{action.encryptionModeString()}} - %div - .mt-5{"ng-if" => "action.repeat_mode"} - %button.light{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable - %button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable - %button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension)"} - Perform Action - .spinner.mb-5.centered.center-align.block{"ng-if" => "action.running"} - %p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"} - Last run {{action.lastExecuted | appDateTime}} - %label.red{"ng-if" => "action.error"} - Error performing action. + %div + .mt-5{"ng-if" => "action.repeat_mode"} + %button.light{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable + %button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable + %button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension)"} + Perform Action + .spinner.mb-5.centered.center-align.block{"ng-if" => "action.running"} + %p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"} + Last run {{action.lastExecuted | appDateTime}} + %label.red{"ng-if" => "action.error"} + Error performing action. - %a.block.center-align.mt-10{"ng-click" => "extension.showURL = !extension.showURL"} Show URL - %p.center-align.wrap{"ng-if" => "extension.showURL"} {{extension.url}} - %a.block.center-align.mt-5{"ng-click" => "deleteExtension(extension)"} Remove extension + %a.block.center-align.mt-10{"ng-click" => "extension.showURL = !extension.showURL"} Show URL + %p.center-align.wrap{"ng-if" => "extension.showURL"} {{extension.url}} + %a.block.center-align.mt-5{"ng-click" => "deleteExtension(extension)"} Remove extension - .large-v-space + .large-v-space - %a.block{"ng-click" => "toggleExtensionForm()"} Add new extension + %a.block{"ng-click" => "toggleExtensionForm()"} Add new extension - %form.mt-10.mb-10{"ng-if" => "showNewExtensionForm"} - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'} - %button.btn.dark-button.btn-block{"ng-click" => "submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - Add Extension + %form.mt-10.mb-10{"ng-if" => "showNewExtensionForm"} + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'} + %button.btn.dark-button.btn-block{"ng-click" => "submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + Add Extension - %a.block.mt-5{"ng-click" => "reloadExtensionsPressed()", "ng-if" => "extensionManager.extensions.length > 0"} Reload all extensions - %a.block.mt-5{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions + %a.block.mt-5{"ng-click" => "reloadExtensionsPressed()", "ng-if" => "extensionManager.extensions.length > 0"} Reload all extensions + %a.block.mt-5{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} Available Extensions + + + .white-bg.medium-padding.mt-10.ext-section{"ng-class" => "{'opened': state.showThemes}"} + %h1 + %a{"ng-click" => "state.showThemes = !state.showThemes"} Themes + %div{"ng-if" => "state.showThemes"} + %section{"ng-repeat" => "theme in themeManager.themes"} + %label {{theme.name}} + %p {{theme.url}} + %a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme)"} Activate + %a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme)"} Deactivate + %a.red.ml-2{"ng-click" => "deleteTheme(theme)"} Delete + + %section + %p.bold.mb-5 Install New Theme + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'New Theme URL', :required => true, + :type => 'url', 'ng-model' => 'state.themeUrl', "ng-keyup" => "$event.keyCode == 13 && submitTheme();"} + %a.block.mt-5{"href" => "https://standardnotes.org/extensions/themes", "target" => "_blank"} Available Themes diff --git a/public/dark-blue.css b/public/dark-blue.css new file mode 100644 index 000000000..6661d5619 --- /dev/null +++ b/public/dark-blue.css @@ -0,0 +1,185 @@ +/* General */ + + +body { + color: #d0d0d0 !important; +} + +.app .section > .content { + background-color: #292937 !important; +} + +.section-menu { + background-color: #313142 !important; + color: white !important; +} + +.add-button { + background-color: #2d2d3d !important; +} + +.panel { + background-color: #20202b !important; + color: #d8d8d8 !important; +} + +.dropdown-menu { + background-color: #20202b !important; + color: #d8d8d8 !important; +} + +.nt-dropdown-menu.dark li .text { + color: #d8d8d8 !important; +} + +.blue { + color: #60a8f3 !important; +} + +.gray-bg { + background-color: #20202b !important; + color: #d8d8d8 !important; + border: 1px solid #292937 !important; +} + +.white-bg { + background-color: #313142 !important; + color: white !important; +} + + +/* Tags */ + +.app .tags .content { + background-color: #191922 !important; +} + +#tags-title-bar { + color: #d8d8d8 !important; +} + +.tags .tag { + color: #d6d6e1 !important; +} + +.tags .tag > .info > .title { + color: #d6d6e1 !important; +} + +.tags .tag.selected { + background-color: #292937 !important; +} + +.tags .tag:hover:not(.selected) { + background-color: #292937 !important; +} + + +/* Notes */ + +.app .notes .content { + background-color: #20202b !important; +} + +.notes { + border-left: 1px solid #13131a !important; + border-right: 1px solid #13131a !important; +} + +#notes-title-bar { + color: #d8d8d8 !important; + background-color: #191922 !important; +} + +.notes .filter-section .filter-bar { + background-color: #2d2d3d !important; +} + +.notes .note.selected { + background-color: #292937 !important; + color: #d6d6e1 !important; +} + +.notes .note { + border-bottom: 1px solid #13131a !important; + background-color: #20202b !important; + color: #d6d6e1; +} + + +/* Editor */ + +#editor-title-bar { + background-color: #191922 !important; +} + +#editor-title-bar > .title > .input { + color: #d8d8d8 !important; +} + +.editor-content { + background-color: #202020 !important; +} + +.editor-content .editable { + background-color: #202020; + color: #c7c7e0; +} + +#editor-title-bar .editor-tags .tags-input { + color: #d8d8d8 !important; +} + + + +/* Menus */ + +.menu-section-header { + background-color: #20202b !important; + border-bottom: 1px solid #2f2f2f !important; + color: #d8d8d8 !important; +} + +.dropdown-menu.editor-menu ul li.menu-item { + border-bottom: 1px solid rgba(0, 0, 0, 0.1) !important; + background-color: #20202b !important; +} + +.dropdown-menu.editor-menu ul li.menu-item:hover { + background-color: #292937 !important; + color: #d6d6e1 !important; +} + +.menu-section-footer { + background-color: #20202b !important; + border-top: 1px solid #2f2f2f !important; + color: #d8d8d8 !important; +} + + +/* Footer */ + +#footer-bar { + background-color: #191922 !important; +} + +#footer-bar a { + color: #60a8f3; +} + +.footer-bar-link > a { + color: #dbdbdb !important; +} + +/* UI */ + +button.light { + background-color: #0088ff !important; + border: 0 !important; + color: white !important; +} + +.form-control { + background-color: #2d2d3d !important; + border: 1px solid #13131a !important; +}