#731: UI counterpart

This commit is contained in:
gorhill 2015-02-11 11:34:51 -05:00
parent 287f502321
commit 44768e8dba
5 changed files with 331 additions and 57 deletions

View file

@ -339,6 +339,22 @@
"message":"Apply changes", "message":"Apply changes",
"description":"English: 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": { "rulesEdit": {
"message": "Edit", "message": "Edit",
"description": "Will enable manual-edit mode (textarea)" "description": "Will enable manual-edit mode (textarea)"
@ -356,7 +372,7 @@
"description": "" "description": ""
}, },
"rulesExport": { "rulesExport": {
"message": "Export to file...", "message": "Export to file",
"description": "" "description": ""
}, },
"rulesDefaultFileName": { "rulesDefaultFileName": {

View file

@ -4,15 +4,155 @@ div > p:first-child {
div > p:last-child { div > p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
#rulesEditor {
font-size: small;
width: 48em;
height: 40em;
white-space: pre;
text-align: left;
}
code { code {
background-color: #eee; background-color: #eee;
font: 11px monospace; font: 11px monospace;
padding: 2px 4px; padding: 2px 4px;
} }
#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;
}

View file

@ -10,16 +10,33 @@
<body> <body>
<div>
<p data-i18n="rulesHint"></p> <p data-i18n="rulesHint"></p>
<p data-i18n="rulesFormatHint"></p> <p data-i18n="rulesFormatHint"></p>
<p><button id="rulesApply" disabled="true" data-i18n="1pApplyChanges"></button></p> <div id="diff">
<textarea id="rulesEditor" spellcheck="false" dir="auto"></textarea> <div class="pane left">
<p><button id="importButton" type="button" data-i18n="1pImport"></button> &emsp; <div>
<button id="exportButton" type="button" data-i18n="1pExport"></button> <h2 data-i18n="rulesPermanentHeader"></h2>
<input class="hidden" id="importFilePicker" type="file" accept="text/plain" style="display: none;"></p> <button type="button" id="exportButton" data-i18n="rulesExport"></button>
<span class="hidden" data-i18n="rulesDefaultFileName" style="display: none;"></span> <button type="button" id="revertButton" data-i18n="rulesRevert"></button>
</div> </div>
<ul></ul>
</div>
<div class="pane right">
<div>
<h2 data-i18n="rulesTemporaryHeader"></h2>
<button type="button" id="commitButton" data-i18n="rulesCommit"></button>
<button type="button" id="editEnterButton" data-i18n="rulesEdit"></button>
<button type="button" id="editStopButton" data-i18n="rulesEditSave"></button>
<button type="button" id="editCancelButton" data-i18n="rulesEditDiscard"></button>
<button type="button" id="importButton" data-i18n="rulesImport"></button>
</div>
<textarea spellcheck="false"></textarea>
<ul></ul>
</div>
</div>
<input class="hidden" id="importFilePicker" type="file" accept="text/plain" style="display: none;">
<span class="hidden" data-i18n="rulesDefaultFileName" style="display: none;"></span>
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>

View file

@ -38,7 +38,7 @@ var messager = vAPI.messaging.channel('dyna-rules.js');
var normalizeRawRules = function(s) { var normalizeRawRules = function(s) {
return s.replace(/[ \t]+/g, ' ') return s.replace(/[ \t]+/g, ' ')
.split(/\s*\n+\s*/) .split(/\s*\n+\s*/)
.sort(directiveSort) .sort()
.join('\n') .join('\n')
.trim(); .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) { rules = details.sessionRules.split(/\n+/);
var aIsSwitch = a.indexOf(':') !== -1; i = rules.length;
var bIsSwitch = b.indexOf(':') !== -1; while ( i-- ) {
if ( aIsSwitch === bIsSwitch ) { rule = rules[i].trim();
return a.localeCompare(b); 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) { rules = Object.keys(allRules).sort();
cachedRawRules = normalizeRawRules(rawRules); for ( i = 0; i < rules.length; i++ ) {
uDom('#rulesEditor').val(cachedRawRules); rule = rules[i];
}; onLeft = permanentRules.hasOwnProperty(rule);
onRight = sessionRules.hasOwnProperty(rule);
if ( onLeft && onRight ) {
permanentList.push('<li>', rule);
sessionList.push('<li>', rule);
} else if ( onLeft ) {
permanentList.push('<li>', rule);
sessionList.push('<li class="notRight toRemove">', rule);
} else {
permanentList.push('<li>&nbsp;');
sessionList.push('<li class="notLeft">', rule);
}
}
/******************************************************************************/ uDom('#diff > .left > ul > li').remove();
uDom('#diff > .left > ul').html(permanentList.join(''));
var rulesApplyHandler = function() { uDom('#diff > .right > ul > li').remove();
var onWritten = function(response) { uDom('#diff > .right > ul').html(sessionList.join(''));
processRules(response); uDom('#diff').toggleClass('dirty', details.sessionRules !== details.permanentRules);
rulesChanged();
};
var request = {
what: 'setDynamicRules',
rawRules: uDom('#rulesEditor').val()
};
messager.send(request, onWritten);
}; };
/******************************************************************************/ /******************************************************************************/
@ -106,9 +123,11 @@ function handleImportFilePicker() {
.replace(/\|/g, ' ') .replace(/\|/g, ' ')
.replace(/\n/g, ' * noop\n'); .replace(/\n/g, ' * noop\n');
} }
var textarea = uDom('#rulesEditor'); var request = {
textarea.val([textarea.val(), result].join('\n').trim()); 'what': 'setSessionFirewallRules',
rulesChanged(); 'rules': rulesFromHTML('#diff .right li') + '\n' + result
};
messager.send(request, renderRules);
}; };
var file = this.files[0]; var file = this.files[0];
if ( file === undefined || file.name === '' ) { if ( file === undefined || file.name === '' ) {
@ -141,7 +160,7 @@ function exportUserRulesToFile() {
.replace('{{datetime}}', now.toLocaleString()) .replace('{{datetime}}', now.toLocaleString())
.replace(/ +/g, '_'); .replace(/ +/g, '_');
vAPI.download({ vAPI.download({
'url': 'data:text/plain,' + encodeURIComponent(uDom('#rulesEditor').val()), 'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li')),
'filename': filename, 'filename': filename,
'saveAs': true '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() { uDom.onLoad(function() {
// Handle user interaction // Handle user interaction
uDom('#importButton').on('click', startImportFilePicker); uDom('#importButton').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportButton').on('click', exportUserRulesToFile); 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);
}); });
/******************************************************************************/ /******************************************************************************/

View file

@ -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) { var onMessage = function(request, sender, callback) {
// Async // Async
switch ( request.what ) { switch ( request.what ) {
@ -738,14 +747,19 @@ var onMessage = function(request, sender, callback) {
var response; var response;
switch ( request.what ) { switch ( request.what ) {
case 'getDynamicRules': case 'getFirewallRules':
response = µb.permanentFirewall.toString(); response = getFirewallRules();
break; break;
case 'setDynamicRules': case 'setSessionFirewallRules':
µb.permanentFirewall.fromString(request.rawRules); µb.sessionFirewall.fromString(request.rules);
response = getFirewallRules();
break;
case 'setPermanentFirewallRules':
µb.permanentFirewall.fromString(request.rules);
µb.savePermanentFirewallRules(); µb.savePermanentFirewallRules();
response = µb.permanentFirewall.toString(); response = getFirewallRules();
break; break;
default: default: