diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index b0838d159..5c88964e5 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -339,6 +339,22 @@ "message":"Apply changes", "description":"English: Apply changes" }, + "rulesPermanentHeader": { + "message": "Permanent rules", + "description": "header" + }, + "rulesTemporaryHeader": { + "message": "Temporary rules", + "description": "header" + }, + "rulesRevert": { + "message": "Revert", + "description": "This will remove all temporary rules" + }, + "rulesCommit": { + "message": "Commit", + "description": "This will persist temporary rules" + }, "rulesEdit": { "message": "Edit", "description": "Will enable manual-edit mode (textarea)" @@ -356,7 +372,7 @@ "description": "" }, "rulesExport": { - "message": "Export to file...", + "message": "Export to file", "description": "" }, "rulesDefaultFileName": { diff --git a/src/css/dyna-rules.css b/src/css/dyna-rules.css index 2a3a134e5..fdd878155 100644 --- a/src/css/dyna-rules.css +++ b/src/css/dyna-rules.css @@ -4,15 +4,155 @@ div > p:first-child { div > p:last-child { margin-bottom: 0; } -#rulesEditor { - font-size: small; - width: 48em; - height: 40em; - white-space: pre; - text-align: left; - } code { background-color: #eee; font: 11px monospace; padding: 2px 4px; -} \ No newline at end of file +} +#diff { + border: 0; + border-top: 1px solid #eee; + margin: 0; + padding: 0.5em 0 0 0; + white-space: nowrap; +} +#diff > .pane { + border: 0; + box-sizing: box-border; + display: inline-block; + font: 12px/1.4 monospace; + margin: 0; + padding: 0; + position: relative; + white-space: normal; + width: calc(50% - 2px); + } +#diff > .pane > div { + padding: 0 0 1em 0; + text-align: center; + } +#diff > .pane > div > span { + float: left; + } +body[dir="ltr"] #revertButton:after { + content: '\2009\f061'; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + vertical-align: baseline; + display: inline-block; + } +body[dir="rtl"] #revertButton:after { + content: '\2009\f060'; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + vertical-align: baseline; + display: inline-block; + } +body[dir="ltr"] #commitButton:before { + content: '\f060\2009'; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + vertical-align: baseline; + display: inline-block; + } +body[dir="rtl"] #commitButton:before { + content: '\f061\2009'; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + vertical-align: baseline; + display: inline-block; + } +#revertButton, +#commitButton, +#diff.edit #editEnterButton { + opacity: 0.25; + pointer-events: none; + } +#editStopButton, +#editCancelButton { + display: none; + } +#diff.dirty:not(.edit) #revertButton, +#diff.dirty:not(.edit) #commitButton { + opacity: 1; + pointer-events: auto; + } +#diff.edit #editStopButton, +#diff.edit #editCancelButton { + display: initial; + } +#diff.edit #importButton, +#diff.edit #exportButton { + display: none; + } +#diff ul { + border: 0; + border-top: 1px solid #eee; + list-style-type: none; + margin: 0; + overflow: hidden; + padding: 1em 0 0 0; + } +#diff.edit .right ul { + visibility: hidden; + } +#diff .left { + padding: 0 0 0 0; + } +#diff .right > ul { + color: #888; + } +#diff li { + background-color: white; + direction: ltr; + padding: 2px 0; + text-align: left; + white-space: nowrap; + } +#diff li:nth-of-type(2n+0) { + background-color: #eee; + } +#diff .right li { + } +#diff .right li:hover { + } +#diff .right li.notLeft { + color: #000; + } +#diff .right li.notRight { + color: #000; + } +#diff .right li.toRemove { + color: #000; + text-decoration: line-through; + } +#diff textarea { + background-color: #f8f8ff; + border: 0; + border-top: 1px solid #eee; + direction: ltr; + font: 12px monospace; + line-height: calc(140% + 4px); + height: 100%; + left: 0; + margin: 0; + overflow: hidden; + overflow-y: auto; + padding: 1em 0 0 0; + position: absolute; + resize: none; + visibility: hidden; + white-space: nowrap; + width: 100%; + } +#diff.edit textarea { + visibility: visible; + } diff --git a/src/dyna-rules.html b/src/dyna-rules.html index 0f161ca04..409c9fbbf 100644 --- a/src/dyna-rules.html +++ b/src/dyna-rules.html @@ -10,16 +10,33 @@ -
-

-

-

- -

  - -

- -
+

+

+
+
+
+

+ + +
+ +
+
+
+

+ + + + + +
+ + +
+
+ + + diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js index 51575c171..13e5aae61 100644 --- a/src/js/dyna-rules.js +++ b/src/js/dyna-rules.js @@ -38,7 +38,7 @@ var messager = vAPI.messaging.channel('dyna-rules.js'); var normalizeRawRules = function(s) { return s.replace(/[ \t]+/g, ' ') .split(/\s*\n+\s*/) - .sort(directiveSort) + .sort() .join('\n') .trim(); }; @@ -58,36 +58,53 @@ var cachedRawRules = ''; /******************************************************************************/ -// Switches before, rules after +var renderRules = function(details) { + var rules, rule, i; + var permanentList = []; + var sessionList = []; + var allRules = {}; + var permanentRules = {}; + var sessionRules = {}; + var onLeft, onRight; -var directiveSort = function(a, b) { - var aIsSwitch = a.indexOf(':') !== -1; - var bIsSwitch = b.indexOf(':') !== -1; - if ( aIsSwitch === bIsSwitch ) { - return a.localeCompare(b); + rules = details.sessionRules.split(/\n+/); + i = rules.length; + while ( i-- ) { + rule = rules[i].trim(); + sessionRules[rule] = allRules[rule] = true; } - return aIsSwitch ? -1 : 1; -}; + details.sessionRules = rules.sort().join('\n'); -/******************************************************************************/ + rules = details.permanentRules.split(/\n+/); + i = rules.length; + while ( i-- ) { + rule = rules[i].trim(); + permanentRules[rule] = allRules[rule] = true; + } + details.permanentRules = rules.sort().join('\n'); -var processRules = function(rawRules) { - cachedRawRules = normalizeRawRules(rawRules); - uDom('#rulesEditor').val(cachedRawRules); -}; + rules = Object.keys(allRules).sort(); + for ( i = 0; i < rules.length; i++ ) { + rule = rules[i]; + onLeft = permanentRules.hasOwnProperty(rule); + onRight = sessionRules.hasOwnProperty(rule); + if ( onLeft && onRight ) { + permanentList.push('
  • ', rule); + sessionList.push('
  • ', rule); + } else if ( onLeft ) { + permanentList.push('
  • ', rule); + sessionList.push('
  • ', rule); + } else { + permanentList.push('
  •  '); + sessionList.push('
  • ', rule); + } + } -/******************************************************************************/ - -var rulesApplyHandler = function() { - var onWritten = function(response) { - processRules(response); - rulesChanged(); - }; - var request = { - what: 'setDynamicRules', - rawRules: uDom('#rulesEditor').val() - }; - messager.send(request, onWritten); + uDom('#diff > .left > ul > li').remove(); + uDom('#diff > .left > ul').html(permanentList.join('')); + uDom('#diff > .right > ul > li').remove(); + uDom('#diff > .right > ul').html(sessionList.join('')); + uDom('#diff').toggleClass('dirty', details.sessionRules !== details.permanentRules); }; /******************************************************************************/ @@ -106,9 +123,11 @@ function handleImportFilePicker() { .replace(/\|/g, ' ') .replace(/\n/g, ' * noop\n'); } - var textarea = uDom('#rulesEditor'); - textarea.val([textarea.val(), result].join('\n').trim()); - rulesChanged(); + var request = { + 'what': 'setSessionFirewallRules', + 'rules': rulesFromHTML('#diff .right li') + '\n' + result + }; + messager.send(request, renderRules); }; var file = this.files[0]; if ( file === undefined || file.name === '' ) { @@ -141,7 +160,7 @@ function exportUserRulesToFile() { .replace('{{datetime}}', now.toLocaleString()) .replace(/ +/g, '_'); vAPI.download({ - 'url': 'data:text/plain,' + encodeURIComponent(uDom('#rulesEditor').val()), + 'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li')), 'filename': filename, 'saveAs': true }); @@ -149,15 +168,83 @@ function exportUserRulesToFile() { /******************************************************************************/ +var rulesFromHTML = function(selector) { + var rules = []; + var lis = uDom(selector); + var li; + for ( var i = 0; i < lis.length; i++ ) { + li = lis.at(i); + if ( li.hasClassName('toRemove') ) { + rules.push(''); + } else { + rules.push(li.text()); + } + } + return rules.join('\n'); +}; + +/******************************************************************************/ + +var revertHandler = function() { + var request = { + 'what': 'setSessionFirewallRules', + 'rules': rulesFromHTML('#diff .left li') + }; + messager.send(request, renderRules); +}; + +/******************************************************************************/ + +var commitHandler = function() { + var request = { + 'what': 'setPermanentFirewallRules', + 'rules': rulesFromHTML('#diff .right li') + }; + messager.send(request, renderRules); +}; + +/******************************************************************************/ + +var editStartHandler = function(ev) { + uDom('#diff .right textarea').val(rulesFromHTML('#diff .right li')); + var parent = uDom(this).ancestors('#diff'); + parent.toggleClass('edit', true); +}; + +/******************************************************************************/ + +var editStopHandler = function(ev) { + var parent = uDom(this).ancestors('#diff'); + parent.toggleClass('edit', false); + var request = { + 'what': 'setSessionFirewallRules', + 'rules': uDom('#diff .right textarea').val() + }; + messager.send(request, renderRules); +}; + +/******************************************************************************/ + +var editCancelHandler = function(ev) { + var parent = uDom(this).ancestors('#diff'); + parent.toggleClass('edit', false); +}; + +/******************************************************************************/ + uDom.onLoad(function() { // Handle user interaction uDom('#importButton').on('click', startImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#exportButton').on('click', exportUserRulesToFile); - uDom('#rulesEditor').on('input', rulesChanged); - uDom('#rulesApply').on('click', rulesApplyHandler); - messager.send({ what: 'getDynamicRules' }, processRules); + uDom('#revertButton').on('click', revertHandler) + uDom('#commitButton').on('click', commitHandler) + uDom('#editEnterButton').on('click', editStartHandler) + uDom('#editStopButton').on('click', editStopHandler) + uDom('#editCancelButton').on('click', editCancelHandler) + + messager.send({ what: 'getFirewallRules' }, renderRules); }); /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index af8a3bd6a..bd69e59aa 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -727,6 +727,15 @@ var µb = µBlock; /******************************************************************************/ +var getFirewallRules = function() { + return { + permanentRules: µb.permanentFirewall.toString(), + sessionRules: µb.sessionFirewall.toString() + }; +}; + +/******************************************************************************/ + var onMessage = function(request, sender, callback) { // Async switch ( request.what ) { @@ -738,14 +747,19 @@ var onMessage = function(request, sender, callback) { var response; switch ( request.what ) { - case 'getDynamicRules': - response = µb.permanentFirewall.toString(); + case 'getFirewallRules': + response = getFirewallRules(); break; - case 'setDynamicRules': - µb.permanentFirewall.fromString(request.rawRules); + case 'setSessionFirewallRules': + µb.sessionFirewall.fromString(request.rules); + response = getFirewallRules(); + break; + + case 'setPermanentFirewallRules': + µb.permanentFirewall.fromString(request.rules); µb.savePermanentFirewallRules(); - response = µb.permanentFirewall.toString(); + response = getFirewallRules(); break; default: