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: