no extensions box

This commit is contained in:
Mo Bitar
2017-05-16 11:55:52 -05:00
parent b65ed8c90c
commit 5ec4797d85
13 changed files with 315 additions and 156 deletions

View File

@@ -8,4 +8,14 @@ class BaseCtrl {
} }
} }
function getParameterByName(name, url) {
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
angular.module('app.frontend').controller('BaseCtrl', BaseCtrl); angular.module('app.frontend').controller('BaseCtrl', BaseCtrl);

View File

@@ -55,6 +55,11 @@ angular.module('app.frontend')
this.showExtensionsMenu = !this.showExtensionsMenu; this.showExtensionsMenu = !this.showExtensionsMenu;
} }
$timeout(function(){
// testing
this.toggleExtensions();
}.bind(this))
this.toggleIO = function() { this.toggleIO = function() {
this.showIOMenu = !this.showIOMenu; this.showIOMenu = !this.showIOMenu;
this.showExtensionsMenu = false; this.showExtensionsMenu = false;

View File

@@ -26,7 +26,7 @@ class ContextualExtensionsMenu {
}) })
} }
$scope.executeAction = function(action, extension) { $scope.executeAction = function(action, extension, parentAction) {
if(!$scope.isActionEnabled(action, extension)) { if(!$scope.isActionEnabled(action, extension)) {
alert("This action requires " + action.access_type + " access to this note. You can change this setting in the Extensions menu on the bottom of the app."); alert("This action requires " + action.access_type + " access to this note. You can change this setting in the Extensions menu on the bottom of the app.");
return; return;
@@ -41,7 +41,13 @@ class ContextualExtensionsMenu {
$scope.handleActionResponse(action, response); $scope.handleActionResponse(action, response);
// reload extension actions // reload extension actions
extensionManager.loadExtensionInContextOfItem(extension, $scope.item, null); extensionManager.loadExtensionInContextOfItem(extension, $scope.item, function(ext){
// keep nested state
if(parentAction) {
var matchingAction = _.find(ext.actions, {label: parentAction.label});
matchingAction.showNestedActions = true;
}
});
}) })
} }

View File

@@ -20,25 +20,6 @@ class EditorMenu {
$scope.callback()(editor); $scope.callback()(editor);
} }
$scope.deleteEditor = function(editor) {
if(confirm("Are you sure you want to delete this editor?")) {
editorManager.deleteEditor(editor);
}
}
$scope.setDefaultEditor = function(editor) {
editorManager.setDefaultEditor(editor);
}
$scope.removeDefaultEditor = function(editor) {
editorManager.removeDefaultEditor(editor);
}
$scope.submitNewEditorRequest = function() {
editorManager.addNewEditorFromURL($scope.formData.url);
$scope.formData = {};
}
} }
} }

View File

@@ -7,37 +7,14 @@ class GlobalExtensionsMenu {
}; };
} }
controller($scope, extensionManager, syncManager, modelManager, themeManager) { controller($scope, extensionManager, syncManager, modelManager, themeManager, editorManager) {
'ngInject'; 'ngInject';
$scope.formData = {};
$scope.extensionManager = extensionManager; $scope.extensionManager = extensionManager;
$scope.themeManager = themeManager; $scope.themeManager = themeManager;
$scope.state = {showDataExts: true, showThemes: true}; $scope.editorManager = editorManager;
$scope.toggleExtensionForm = function() {
$scope.newExtensionData = {};
$scope.showNewExtensionForm = !$scope.showNewExtensionForm;
}
$scope.submitNewExtensionForm = function() {
if($scope.newExtensionData.url) {
extensionManager.addExtension($scope.newExtensionData.url, function(response){
if(!response) {
if($scope.newExtensionData.url.indexOf("type=sf") != -1) {
alert("Unable to register this extension. You are attempting to register a Standard File extension in Standard Notes. You should instead open your Standard File Dashboard and register this extension there.");
} else if($scope.newExtensionData.url.indexOf("name=") != -1) {
// user is mistakenly trying to register editor extension, most likely
alert("Unable to register this extension. It looks like you may be trying to install an editor extension. To do that, click 'Editor' under the current note's title.");
} else {
alert("Unable to register this extension. Make sure the link is valid and try again.");
}
} else {
$scope.newExtensionData.url = "";
$scope.showNewExtensionForm = false;
}
})
}
}
$scope.selectedAction = function(action, extension) { $scope.selectedAction = function(action, extension) {
extensionManager.executeAction(action, extension, null, function(response){ extensionManager.executeAction(action, extension, null, function(response){
@@ -55,7 +32,7 @@ class GlobalExtensionsMenu {
extensionManager.changeExtensionEncryptionFormat(encrypted, extension); extensionManager.changeExtensionEncryptionFormat(encrypted, extension);
} }
$scope.deleteExtension = function(extension) { $scope.deleteActionExtension = function(extension) {
if(confirm("Are you sure you want to delete this extension?")) { if(confirm("Are you sure you want to delete this extension?")) {
extensionManager.deleteExtension(extension); extensionManager.deleteExtension(extension);
} }
@@ -67,11 +44,6 @@ class GlobalExtensionsMenu {
} }
} }
$scope.submitTheme = function() {
themeManager.submitTheme($scope.state.themeUrl);
$scope.state.themeUrl = "";
}
$scope.deleteTheme = function(theme) { $scope.deleteTheme = function(theme) {
if(confirm("Are you sure you want to delete this theme?")) { if(confirm("Are you sure you want to delete this theme?")) {
themeManager.deactivateTheme(theme); themeManager.deactivateTheme(theme);
@@ -80,6 +52,75 @@ class GlobalExtensionsMenu {
} }
} }
// Editors
$scope.deleteEditor = function(editor) {
if(confirm("Are you sure you want to delete this editor?")) {
editorManager.deleteEditor(editor);
}
}
$scope.setDefaultEditor = function(editor) {
editorManager.setDefaultEditor(editor);
}
$scope.removeDefaultEditor = function(editor) {
editorManager.removeDefaultEditor(editor);
}
// Installation
$scope.submitInstallLink = function() {
var link = $scope.formData.installLink;
if(!link) {
return;
}
var completion = function() {
$scope.formData.installLink = "";
}
var type = getParameterByName("type", link);
if(type == "sf" || type == "sync") {
$scope.handleSyncAdapterLink(link, completion);
} else if(type == "editor") {
$scope.handleEditorLink(link, completion);
} else if(link.indexOf(".css") != -1 || type == "theme") {
$scope.handleThemeLink(link, completion);
} else {
$scope.handleActionLink(link, completion);
}
}
$scope.handleSyncAdapterLink = function(link, completion) {
completion();
}
$scope.handleThemeLink = function(link, completion) {
themeManager.submitTheme(link);
completion();
}
$scope.handleActionLink = function(link, completion) {
if(link) {
extensionManager.addExtension(link, function(response){
if(!response) {
alert("Unable to register this extension. Make sure the link is valid and try again.");
} else {
completion();
}
})
}
}
$scope.handleEditorLink = function(link, completion) {
editorManager.addNewEditorFromURL(link);
completion();
}
} }
} }

View File

@@ -1,6 +1,5 @@
class EditorManager { class EditorManager {
constructor($rootScope, modelManager, syncManager) { constructor($rootScope, modelManager, syncManager) {
this.syncManager = syncManager; this.syncManager = syncManager;
this.modelManager = modelManager; this.modelManager = modelManager;
@@ -79,7 +78,7 @@ class EditorManager {
} }
addNewEditorFromURL(url) { addNewEditorFromURL(url) {
var name = this.getParameterByName("name", url); var name = getParameterByName("name", url);
var editor = this.modelManager.createItem({ var editor = this.modelManager.createItem({
content_type: this.editorType, content_type: this.editorType,
url: url, url: url,
@@ -96,15 +95,6 @@ class EditorManager {
this.syncManager.sync(); this.syncManager.sync();
} }
getParameterByName(name, url) {
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
} }
angular.module('app.frontend').service('editorManager', EditorManager); angular.module('app.frontend').service('editorManager', EditorManager);

View File

@@ -84,6 +84,10 @@ class ThemeManager {
} }
displayNameForThemeFile(fileName) { displayNameForThemeFile(fileName) {
let fromParam = getParameterByName("name", fileName);
if(fromParam) {
return fromParam;
}
let name = fileName.split(".")[0]; let name = fileName.split(".")[0];
let cleaned = name.split("-").join(" "); let cleaned = name.split("-").join(" ");
return this.capitalizeString(cleaned); return this.capitalizeString(cleaned);

View File

@@ -10,6 +10,7 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-color: rgba(gray, 0.3); background-color: rgba(gray, 0.3);
color: black;
.content { .content {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
@@ -23,3 +24,90 @@
overflow-y: scroll; overflow-y: scroll;
} }
} }
#global-ext-menu {
color: black;
.panel-body {
padding: 0;
}
.container {
padding: 13px 18px;
&.no-bottom {
padding-bottom: 0;
}
}
p {
font-size: 14px;
&.small {
font-size: 12px;
}
}
.link-group {
a {
margin-right: 2px;
}
}
.blue-box {
background-color: $blue-color;
color: white;
border-radius: 4px;
padding: 16px 20px;
button {
background-color: white;
color: $blue-color;
border-radius: 3px;
font-weight: bold;
padding: 6px 20px;
width: 100%;
&:hover {
text-decoration: underline;
}
}
}
.dashboard-link {
padding-top: 12px;
font-weight: normal;
}
.section-margin {
margin-top: 20px;
}
input {
border: 1px solid $blue-color;
border-radius: 2px;
}
ul {
border-top: 1px solid $light-bg-color;
border-bottom: 1px solid $light-bg-color;
margin: 0;
padding: 0;
margin-top: 12px;
li {
cursor: pointer;
background-color: rgba($light-bg-color, 0.2);
&:hover {
background-color: rgba($light-bg-color, 0.4);
}
&:not(:last-child) {
border-bottom: 1px solid $light-bg-color;
}
}
}
}
ul {
margin: 0;
padding: 0;
}

View File

@@ -142,9 +142,20 @@ ul.section-menu-bar {
&:hover { &:hover {
background-color: $blue-color; background-color: $blue-color;
.blue { .blue {
color: white; color: white;
} }
&.nested-hover {
color: black;
background-color: $light-bg-color;
}
}
&.nested-hover {
color: black;
background-color: white;
} }
.menu-item-title { .menu-item-title {

View File

@@ -1,3 +1,11 @@
.selectable {
user-select: all;
}
.clear {
clear: both;
}
.pull-left { .pull-left {
float: left !important; float: left !important;
} }
@@ -166,7 +174,7 @@
} }
.small { .small {
font-size: 10px !important; font-size: 10px;
} }
.medium { .medium {

View File

@@ -18,7 +18,7 @@
%div{"ng-if" => "action.showNestedActions"} %div{"ng-if" => "action.showNestedActions"}
%ul.mt-10 %ul.mt-10
%li.menu-item.white-bg{"ng-repeat" => "subaction in action.subactions", "ng-click" => "executeAction(subaction, extension);", "style" => "margin-top: -1px;"} %li.menu-item.white-bg.nested-hover{"ng-repeat" => "subaction in action.subactions", "ng-click" => "executeAction(subaction, extension, action); $event.stopPropagation();", "style" => "margin-top: -1px;"}
.menu-item-title {{subaction.label}} .menu-item-title {{subaction.label}}
.menu-item-subtitle {{subaction.desc}} .menu-item-subtitle {{subaction.desc}}
%span{"ng-if" => "subaction.running"} %span{"ng-if" => "subaction.running"}

View File

@@ -12,18 +12,7 @@
.subtitle Can access your current note decrypted. .subtitle Can access your current note decrypted.
%ul %ul
%li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"} %li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"}
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
.left-side .menu-item-title
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy {{editor.name}}
.menu-item-title {{editor.name}} %span.inline.blue{"style" => "margin-left: 8px;", "ng-if" => "selectedEditor === editor"} ✓
%a.faded{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Set Default
%a.blue{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove Default
%a.faded{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show URL
.menu-item-subtitle.wrap.mt-5{"ng-if" => "editor.showUrl"} {{editor.url}}
%span.inline.ml-10{"ng-if" => "selectedEditor === editor"} ✓
.right-side
%button.white.medium{"style" => "width: 50px; height: 40px;", "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} ☓
.footer.mt-10
%input.form-control{"ng-model" => "formData.url", "placeholder" => "Add new editor via URL", "ng-keyup" => "$event.keyCode == 13 && submitNewEditorRequest()"}
%a.block.blue{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} Available Editors

View File

@@ -1,82 +1,108 @@
.panel.panel-default.account-panel.panel-right .panel.panel-default.account-panel.panel-right#global-ext-menu
.panel-body .panel-body
.white-bg.medium-padding.ext-section{"ng-class" => "{'opened': state.showDataExts}"} .container
%h1 .float-group.h20
%a{"ng-click" => "state.showDataExts = !state.showDataExts"} Action Extensions %h1.blue.pull-left Extensions
%p{"style" => "margin-top: 3px;"} These extensions perform actions on a specific note. Choose "Actions" in the note editor to use installed actions. %a.block.pull-right.dashboard-link{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} Open Dashboard
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !editorManager.externalEditors.length"}
%p Customize your experience with editors, themes, and actions.
.blue-box.mt-10
%h3 Available as part of the Extended subscription.
%p.mt-5 Note history
%p.mt-5 Automated backups
%p.mt-5 All editors, themes, and actions
%a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"}
%button.mt-10
%h3 Learn More
%div.mt-10{"ng-if" => "state.showDataExts"} %div{"ng-if" => "themeManager.themes.length > 0"}
%div{"style" => "font-size: 15px;", "ng-if" => "!extensionManager.extensions.length"} No extensions installed .container.no-bottom.section-margin
%div{"ng-if" => "extensionManager.extensions.length"} %h2 Themes
%section.gray-bg.inline-h.mb-10.medium-padding{"ng-repeat" => "extension in extensionManager.extensions | orderBy: 'name'", "ng-init" => "extension.formData = {}"} %ul
%li{"ng-repeat" => "theme in themeManager.themes", "ng-click" => "theme.showDetails = !theme.showDetails"}
.container
%h3 {{theme.name}}
%a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate
%a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate
%div{"ng-if" => "theme.showDetails"}
.link-group
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
%a{"ng-click" => "theme.showLink = !theme.showLink; $event.stopPropagation();"} Show Link
%p.small.selectable.wrap{"ng-if" => "theme.showLink"}
{{theme.url}}
%div{"ng-if" => "editorManager.externalEditors.length > 0"}
.container.no-bottom.section-margin
%h2 Editors
%p{"style" => "margin-top: 3px;"} Choose "Editor" in the note menu to use an editor for a specific note.
%ul
%li{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "editor.showDetails = !editor.showDetails"}
.container
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
%h3 {{editor.name}}
%div.mt-5{"ng-if" => "editor.showDetails"}
.link-group
%a{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Make Default
%a.blue{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default
%a{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show Link
%a.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete
.wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}}
%div{"ng-if" => "extensionManager.extensions.length"}
.container.no-bottom.section-margin
%h2 Actions
%p{"style" => "margin-top: 3px;"} Choose "Actions" in the note editor to use installed actions.
%ul
%li{"ng-repeat" => "extension in extensionManager.extensions | orderBy: 'name'", "ng-init" => "extension.formData = {}", "ng-click" => "extension.showDetails = !extension.showDetails"}
.container
%h3 {{extension.name}} %h3 {{extension.name}}
%p{"ng-if" => "extension.description"} {{extension.description}} %p.small{"ng-if" => "extension.description"} {{extension.description}}
.mt-10 %div{"ng-if" => "extension.showDetails"}
%label.block Access Type .mt-10
%label.normal.block %label.block Access Type
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"} %label.normal.block
Encrypted %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"}
%label.normal.block Encrypted
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"} %label.normal.block
Decrypted %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()"} %ul{"ng-repeat" => "action in extension.actionsInGlobalContext()"}
%label.block {{action.label}} %li
%em{"style" => "font-style: italic;"} {{action.desc}} %label.block {{action.label}}
%em{"ng-if" => "action.repeat_mode == 'watch'"} %em{"style" => "font-style: italic;"} {{action.desc}}
Repeats when a change is made to your items. %em{"ng-if" => "action.repeat_mode == 'watch'"}
%em{"ng-if" => "action.repeat_mode == 'loop'"} Repeats when a change is made to your items.
Repeats at most once every {{action.repeat_timeout}} seconds %em{"ng-if" => "action.repeat_mode == 'loop'"}
%div Repeats at most once every {{action.repeat_timeout}} seconds
%a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} %div
%div{"ng-if" => "action.showPermissions"} %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}}
{{action.permissionsString()}} %div{"ng-if" => "action.showPermissions"}
%label.block.normal {{action.encryptionModeString()}} {{action.permissionsString()}}
%label.block.normal {{action.encryptionModeString()}}
%div %div
.mt-5{"ng-if" => "action.repeat_mode"} .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.disableRepeatAction(action, extension); $event.stopPropagation();"} Disable
%button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable %button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension); $event.stopPropagation();"} Enable
%button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension)"} %button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension); $event.stopPropagation();"}
Perform Action Perform Action
.spinner.mb-5.block{"ng-if" => "action.running"} .spinner.mb-5.block{"ng-if" => "action.running"}
%p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"} %p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"}
Last run {{action.lastExecuted | appDateTime}} Last run {{action.lastExecuted | appDateTime}}
%label.red{"ng-if" => "action.error"} %label.red{"ng-if" => "action.error"}
Error performing action. Error performing action.
%a.block.mt-5{"ng-click" => "extension.showURL = !extension.showURL"} Show URL %a.block.mt-5{"ng-click" => "extension.showURL = !extension.showURL; $event.stopPropagation();"} Show Link
%p.wrap{"ng-if" => "extension.showURL"} {{extension.url}} %p.wrap.selectable.small{"ng-if" => "extension.showURL"} {{extension.url}}
%a.block.mt-5{"ng-click" => "deleteExtension(extension)"} Remove extension %a.block.mt-5{"ng-click" => "deleteActionExtension(extension); $event.stopPropagation();"} Remove extension
.large-v-space .container.section-margin
%h2.blue Install
%a.block{"ng-click" => "toggleExtensionForm()"} Add New Extension %p.faded Enter an install link
%form.mt-10.mb-10
%form.mt-10.mb-10{"ng-if" => "showNewExtensionForm"} %input.form-control{:autofocus => 'autofocus', :name => 'url', :required => true,
%input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'} :type => 'url', 'ng-model' => 'formData.installLink', "ng-keyup" => "$event.keyCode == 13 && submitInstallLink();"}
%button.ui-button.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"} 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