sync feature (#80): draft

This commit is contained in:
gorhill 2015-08-11 15:29:14 -04:00
parent a7712116a7
commit 690421aead
18 changed files with 615 additions and 88 deletions

View file

@ -53,12 +53,14 @@ vAPI.app.restart = function() {
chrome.runtime.reload(); chrome.runtime.reload();
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
// chrome.storage.local.get(null, function(bin){ console.debug('%o', bin); }); // chrome.storage.local.get(null, function(bin){ console.debug('%o', bin); });
vAPI.storage = chrome.storage.local; vAPI.storage = chrome.storage.local;
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
// https://github.com/gorhill/uMatrix/issues/234 // https://github.com/gorhill/uMatrix/issues/234
@ -101,6 +103,7 @@ vAPI.browserSettings = {
} }
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.tabs = {}; vAPI.tabs = {};
@ -499,6 +502,7 @@ vAPI.tabs.injectScript = function(tabId, details, callback) {
} }
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
// Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8 // Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8
@ -534,6 +538,7 @@ vAPI.setIcon = function(tabId, iconStatus, badge) {
chrome.browserAction.setIcon({ tabId: tabId, path: iconPaths }, onIconReady); chrome.browserAction.setIcon({ tabId: tabId, path: iconPaths }, onIconReady);
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.messaging = { vAPI.messaging = {
@ -751,6 +756,7 @@ vAPI.messaging.broadcast = function(message) {
} }
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.net = {}; vAPI.net = {};
@ -875,6 +881,7 @@ vAPI.net.registerListeners = function() {
); );
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.contextMenu = { vAPI.contextMenu = {
@ -890,12 +897,14 @@ vAPI.contextMenu = {
} }
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.lastError = function() { vAPI.lastError = function() {
return chrome.runtime.lastError; return chrome.runtime.lastError;
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
// This is called only once, when everything has been loaded in memory after // This is called only once, when everything has been loaded in memory after
@ -947,6 +956,7 @@ vAPI.onLoadAllCompleted = function() {
chrome.tabs.query({ url: '<all_urls>' }, bindToTabs); chrome.tabs.query({ url: '<all_urls>' }, bindToTabs);
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.punycodeHostname = function(hostname) { vAPI.punycodeHostname = function(hostname) {
@ -957,6 +967,196 @@ vAPI.punycodeURL = function(url) {
return url; return url;
}; };
/******************************************************************************/
/******************************************************************************/
vAPI.cloud = (function() {
var chunkCountPerFetch = 16; // Must be a power of 2
// Mind chrome.storage.sync.MAX_ITEMS (512 at time of writing)
var maxChunkCountPerItem = Math.floor(512 * 0.75) & ~(chunkCountPerFetch - 1);
// Mind chrome.storage.sync.QUOTA_BYTES_PER_ITEM (8192 at time of writing)
var maxChunkSize = Math.floor(chrome.storage.sync.QUOTA_BYTES_PER_ITEM * 0.75);
// Mind chrome.storage.sync.QUOTA_BYTES_PER_ITEM (8192 at time of writing)
var maxStorageSize = chrome.storage.sync.QUOTA_BYTES;
var options = {
deviceName: ''
};
var getDeviceName = function() {
// Assign a permanent user-friendly id to this uBlock instance if one does
// not exist. This will allow to have some sort of identifier for a user
// to possibly identify the source of cloud data.
var name = window.localStorage.getItem('deviceName') || '';
if ( name !== '' ) {
return name;
}
return window.navigator.platform;
};
// This is used to find out a rough count of how many chunks exists:
// We "poll" at specific index in order to get a rough idea of how
// large is the stored string.
// This allows reading a single item with only 2 sync operations -- a
// good thing given chrome.storage.syncMAX_WRITE_OPERATIONS_PER_MINUTE
// and chrome.storage.syncMAX_WRITE_OPERATIONS_PER_HOUR.
var getCoarseChunkCount = function(dataKey, callback) {
var bin = {};
for ( var i = 0; i < maxChunkCountPerItem; i += 16 ) {
bin[dataKey + i.toString()] = '';
}
chrome.storage.sync.get(bin, function(bin) {
if ( chrome.runtime.lastError ) {
callback(0, chrome.runtime.lastError.message);
return;
}
// Could loop backward... let's assume for now
// maxChunkCountPerItem could be something else than a
// multiple of 16.
var chunkCount = 0;
for ( var i = 0; i < maxChunkCountPerItem; i += 16 ) {
if ( bin[dataKey + i.toString()] === '' ) {
break;
}
chunkCount = i + 16;
}
callback(chunkCount);
});
};
var deleteChunks = function(dataKey, start) {
var keys = [];
// No point in deleting more than:
// - The max number of chunks per item
// - The max number of chunks per storage limit
var n = Math.min(
maxChunkCountPerItem,
Math.ceil(maxStorageSize / maxChunkSize)
);
for ( var i = start; i < n; i++ ) {
keys.push(dataKey + i.toString());
}
chrome.storage.sync.remove(keys);
};
var start = function(/* dataKeys */) {
};
var push = function(dataKey, data, callback) {
var item = JSON.stringify({
'source': getDeviceName(),
'tstamp': Date.now(),
'data': data
});
// Chunkify taking into account QUOTA_BYTES_PER_ITEM:
// https://developer.chrome.com/extensions/storage#property-sync
// "The maximum size (in bytes) of each individual item in sync
// "storage, as measured by the JSON stringification of its value
// "plus its key length."
var bin = {};
var chunkCount = Math.ceil(item.length / maxChunkSize);
for ( var i = 0; i < chunkCount; i++ ) {
bin[dataKey + i.toString()] = item.substr(i * maxChunkSize, maxChunkSize);
}
bin[dataKey + i.toString()] = ''; // Sentinel
chrome.storage.sync.set(bin, function() {
var errorStr;
if ( chrome.runtime.lastError ) {
errorStr = chrome.runtime.lastError.message;
}
callback(errorStr);
// Remove potentially unused trailing chunks
deleteChunks(dataKey, chunkCount);
});
};
var pull = function(dataKey, callback) {
var assembleChunks = function(bin) {
if ( chrome.runtime.lastError ) {
callback(null, chrome.runtime.lastError.message);
return;
}
// Assemble chunks into a single string.
var json = [], jsonSlice;
var i = 0;
for (;;) {
jsonSlice = bin[dataKey + i.toString()];
if ( jsonSlice === '' ) {
break;
}
json.push(jsonSlice);
i += 1;
}
var entry = null;
try {
entry = JSON.parse(json.join(''));
} catch(ex) {
}
callback(entry);
};
var fetchChunks = function(coarseCount, errorStr) {
if ( coarseCount === 0 || typeof errorStr === 'string' ) {
callback(null, errorStr);
return;
}
var bin = {};
for ( var i = 0; i < coarseCount; i++ ) {
bin[dataKey + i.toString()] = '';
}
chrome.storage.sync.get(bin, assembleChunks);
};
getCoarseChunkCount(dataKey, fetchChunks);
};
var getOptions = function(callback) {
if ( typeof callback !== 'function' ) {
return;
}
callback(options);
};
var setOptions = function(details, callback) {
if ( typeof details !== 'object' || details === null ) {
return;
}
if ( typeof details.deviceName === 'string' ) {
window.localStorage.setItem('deviceName', details.deviceName);
}
if ( typeof callback === 'function' ) {
callback(options);
}
};
return {
start: start,
push: push,
pull: pull,
getOptions: getOptions,
setOptions: setOptions
};
})();
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
})(); })();

View file

@ -2807,6 +2807,7 @@ vAPI.contextMenu.remove = function() {
this.onCommand = null; this.onCommand = null;
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
var optionsObserver = { var optionsObserver = {
@ -2849,6 +2850,7 @@ var optionsObserver = {
optionsObserver.register(); optionsObserver.register();
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.lastError = function() { vAPI.lastError = function() {
@ -2875,6 +2877,7 @@ vAPI.onLoadAllCompleted = function() {
} }
}; };
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
// Likelihood is that we do not have to punycode: given punycode overhead, // Likelihood is that we do not have to punycode: given punycode overhead,
@ -2894,6 +2897,120 @@ vAPI.punycodeURL = function(url) {
return url; return url;
}; };
/******************************************************************************/
/******************************************************************************/
vAPI.cloud = (function() {
var extensionBranchPath = 'extensions.' + location.host;
var cloudBranchPath = extensionBranchPath + '.cloudStorage';
var options = {
deviceName: ''
};
var getDeviceName = function() {
var name = '';
// User-supplied device name.
var branch = Services.prefs.getBranch(extensionBranchPath + '.');
try {
name = branch.getCharPref('deviceName');
} catch(ex) {
}
options.deviceName = name;
if ( name !== '' ) {
return name;
}
// No name: try to use device name specified by user in Preferences.
branch = Services.prefs.getBranch('services.sync.client.');
try {
name = branch.getCharPref('name');
} catch(ex) {
}
if ( name !== '' ) {
return name;
}
// No name: use os/cpu.
return window.navigator.platform || window.navigator.oscpu;
};
var start = function(dataKeys) {
var extensionBranch = Services.prefs.getBranch(extensionBranchPath + '.');
var syncBranch = Services.prefs.getBranch('services.sync.prefs.sync.');
// Mark config entries as syncable
var dataKey;
for ( var i = 0; i < dataKeys.length; i++ ) {
dataKey = dataKeys[i];
if ( extensionBranch.prefHasUserValue('cloudStorage.' + dataKey) === false ) {
extensionBranch.setCharPref('cloudStorage.' + dataKey, '');
}
syncBranch.setBoolPref(cloudBranchPath + '.' + dataKey, true);
}
};
var push = function(datakey, data, callback) {
var branch = Services.prefs.getBranch(cloudBranchPath + '.');
var bin = {
'source': getDeviceName(),
'tstamp': Date.now(),
'data': data
};
branch.setCharPref(datakey, JSON.stringify(bin));
if ( typeof callback === 'function' ) {
callback();
}
};
var pull = function(datakey, callback) {
var result = null;
var branch = Services.prefs.getBranch(cloudBranchPath + '.');
try {
var json = branch.getCharPref(datakey);
if ( typeof json === 'string' ) {
result = JSON.parse(json);
}
} catch(ex) {
}
callback(result);
};
var getOptions = function(callback) {
if ( typeof callback !== 'function' ) {
return;
}
callback(options);
};
var setOptions = function(details, callback) {
if ( typeof details !== 'object' || details === null ) {
return;
}
var branch = Services.prefs.getBranch(extensionBranchPath + '.');
if ( typeof details.deviceName === 'string' ) {
branch.setCharPref('deviceName', details.deviceName);
}
if ( typeof callback === 'function' ) {
callback(options);
}
};
return {
start: start,
push: push,
pull: pull,
getOptions: getOptions,
setOptions: setOptions
};
})();
/******************************************************************************/
/******************************************************************************/ /******************************************************************************/
})(); })();

View file

@ -5,11 +5,14 @@
<title>uBlock — Your filters</title> <title>uBlock — Your filters</title>
<link rel="stylesheet" type="text/css" href="css/common.css"> <link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css"> <link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/cloud-ui.css">
<link rel="stylesheet" type="text/css" href="css/1p-filters.css"> <link rel="stylesheet" type="text/css" href="css/1p-filters.css">
</head> </head>
<body> <body>
<div id="cloudWidget" class="hide" data-cloud-entry="myFiltersPane"></div>
<p data-i18n="1pFormatHint"></p> <p data-i18n="1pFormatHint"></p>
<p><button id="userFiltersApply" class="important" type="button" disabled="true" data-i18n="1pApplyChanges"></button></p> <p><button id="userFiltersApply" class="important" type="button" disabled="true" data-i18n="1pApplyChanges"></button></p>
<textarea class="userFilters" id="userFilters" dir="auto" spellcheck="false"></textarea> <textarea class="userFilters" id="userFilters" dir="auto" spellcheck="false"></textarea>
@ -22,6 +25,7 @@
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/1p-filters.js"></script> <script src="js/1p-filters.js"></script>
</body> </body>

View file

@ -5,13 +5,17 @@
<title>uBlock — Ubiquitous rules</title> <title>uBlock — Ubiquitous rules</title>
<link rel="stylesheet" type="text/css" href="css/common.css"> <link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css"> <link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/cloud-ui.css">
<link rel="stylesheet" type="text/css" href="css/3p-filters.css"> <link rel="stylesheet" type="text/css" href="css/3p-filters.css">
</head> </head>
<body> <body>
<button id="buttonApply" class="important disabled" data-i18n="3pApplyChanges"></button> <div id="cloudWidget" class="hide" data-cloud-entry="tpFiltersPane"></div>
<ul id="options">
<div>
<button id="buttonApply" class="important disabled" data-i18n="3pApplyChanges"></button>
<ul id="options">
<li><input type="checkbox" id="autoUpdate"><label data-i18n="3pAutoUpdatePrompt1" for="autoUpdate"></label>&ensp; <li><input type="checkbox" id="autoUpdate"><label data-i18n="3pAutoUpdatePrompt1" for="autoUpdate"></label>&ensp;
<button class="important disabled" id="buttonUpdate" data-i18n="3pUpdateNow"></button> <button class="important disabled" id="buttonUpdate" data-i18n="3pUpdateNow"></button>
<button id="buttonPurgeAll" class="custom disabled" data-i18n="3pPurgeAll"></button> <button id="buttonPurgeAll" class="custom disabled" data-i18n="3pPurgeAll"></button>
@ -19,8 +23,9 @@
<button class="whatisthis"></button> <button class="whatisthis"></button>
<div class="whatisthis-expandable para" data-i18n="3pParseAllABPHideFiltersInfo"></div> <div class="whatisthis-expandable para" data-i18n="3pParseAllABPHideFiltersInfo"></div>
<li><p id="listsOfBlockedHostsPrompt"></p> <li><p id="listsOfBlockedHostsPrompt"></p>
</ul> </ul>
<ul id="lists"></ul> <ul id="lists"></ul>
</div>
<div id="externalListsDiv"> <div id="externalListsDiv">
<p data-i18n="3pExternalListsHint" style="margin: 0 0 0.25em 0; font-size: 13px;"></p> <p data-i18n="3pExternalListsHint" style="margin: 0 0 0.25em 0; font-size: 13px;"></p>
@ -58,6 +63,7 @@
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/3p-filters.js"></script> <script src="js/3p-filters.js"></script>
</body> </body>

View file

@ -175,6 +175,10 @@
"message":"Color-blind friendly", "message":"Color-blind friendly",
"description":"English: Color-blind friendly" "description":"English: Color-blind friendly"
}, },
"settingsCloudStorageEnabledPrompt":{
"message":"Enable cloud storage support",
"description": ""
},
"settingsAdvancedUserPrompt":{ "settingsAdvancedUserPrompt":{
"message":"I am an advanced user (<a href='https:\/\/github.com\/gorhill\/uBlock\/wiki\/Advanced-user-features'>Required reading<\/a>)", "message":"I am an advanced user (<a href='https:\/\/github.com\/gorhill\/uBlock\/wiki\/Advanced-user-features'>Required reading<\/a>)",
"description":"English: " "description":"English: "
@ -591,6 +595,26 @@
"message": "Permanently", "message": "Permanently",
"description": "English: Permanently" "description": "English: Permanently"
}, },
"cloudPush": {
"message": "Export to cloud storage",
"description": ""
},
"cloudPull": {
"message": "Import from cloud storage",
"description": ""
},
"cloudNoData": {
"message": "...\n...",
"description": ""
},
"cloudDeviceNamePrompt": {
"message": "This device name:",
"description": ""
},
"genericSubmit": {
"message": "Submit",
"description": ""
},
"dummy":{ "dummy":{
"message":"This entry must be the last one", "message":"This entry must be the last one",
"description":"so we dont need to deal with comma for last entry" "description":"so we dont need to deal with comma for last entry"

View file

@ -97,10 +97,8 @@ button.custom:hover {
} }
#buttonApply { #buttonApply {
display: initial; display: initial;
margin: 1em 0;
position: fixed; position: fixed;
right: 1em; right: 1em;
top: 0;
z-index: 10; z-index: 10;
} }
body[dir=rtl] #buttonApply { body[dir=rtl] #buttonApply {

View file

@ -5,11 +5,14 @@
<title>uBlock — Dynamic filtering rules</title> <title>uBlock — Dynamic filtering rules</title>
<link rel="stylesheet" type="text/css" href="css/common.css"> <link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css"> <link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/cloud-ui.css">
<link rel="stylesheet" type="text/css" href="css/dyna-rules.css"> <link rel="stylesheet" type="text/css" href="css/dyna-rules.css">
</head> </head>
<body> <body>
<div id="cloudWidget" class="hide" data-cloud-entry="myRulesPane"></div>
<p data-i18n="rulesHint"></p> <p data-i18n="rulesHint"></p>
<p data-i18n="rulesFormatHint"></p> <p data-i18n="rulesFormatHint"></p>
<div id="diff"> <div id="diff">
@ -51,6 +54,7 @@
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/dyna-rules.js"></script> <script src="js/dyna-rules.js"></script>
</body> </body>

View file

@ -40,10 +40,8 @@ var messager = vAPI.messaging.channel('1p-filters.js');
// This is to give a visual hint that the content of user blacklist has changed. // This is to give a visual hint that the content of user blacklist has changed.
function userFiltersChanged() { function userFiltersChanged() {
uDom('#userFiltersApply').prop( uDom.nodeFromId('userFiltersApply').disabled =
'disabled', uDom('#userFilters').val().trim() === cachedUserFilters;
uDom('#userFilters').val().trim() === cachedUserFilters
);
} }
/******************************************************************************/ /******************************************************************************/
@ -54,7 +52,7 @@ function renderUserFilters() {
return; return;
} }
cachedUserFilters = details.content.trim(); cachedUserFilters = details.content.trim();
uDom('#userFilters').val(details.content); uDom.nodeFromId('userFilters').value = details.content;
}; };
messager.send({ what: 'readUserFilters' }, onRead); messager.send({ what: 'readUserFilters' }, onRead);
} }
@ -160,16 +158,31 @@ var userFiltersApplyHandler = function() {
/******************************************************************************/ /******************************************************************************/
uDom.onLoad(function() { var getCloudData = function() {
// Handle user interaction return uDom.nodeFromId('userFilters').value;
uDom('#importUserFiltersFromFile').on('click', startImportFilePicker); };
uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportUserFiltersToFile').on('click', exportUserFiltersToFile);
uDom('#userFilters').on('input', userFiltersChanged);
uDom('#userFiltersApply').on('click', userFiltersApplyHandler);
renderUserFilters(); var setCloudData = function(data) {
}); if ( typeof data !== 'string' ) {
return;
}
uDom.nodeFromId('userFilters').value = data;
userFiltersChanged();
};
self.cloud.onPush = getCloudData;
self.cloud.onPull = setCloudData;
/******************************************************************************/
// Handle user interaction
uDom('#importUserFiltersFromFile').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportUserFiltersToFile').on('click', exportUserFiltersToFile);
uDom('#userFilters').on('input', userFiltersChanged);
uDom('#userFiltersApply').on('click', userFiltersApplyHandler);
renderUserFilters();
/******************************************************************************/ /******************************************************************************/

View file

@ -495,7 +495,7 @@ var externalListsChangeHandler = function() {
/******************************************************************************/ /******************************************************************************/
var externalListsApplyHandler = function() { var externalListsApplyHandler = function() {
externalLists = uDom('#externalLists').val(); externalLists = uDom.nodeFromId('externalLists').value;
messager.send({ messager.send({
what: 'userSettings', what: 'userSettings',
name: 'externalLists', name: 'externalLists',
@ -520,21 +520,69 @@ var groupEntryClickHandler = function() {
/******************************************************************************/ /******************************************************************************/
uDom.onLoad(function() { var getCloudData = function() {
uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged); var bin = {
uDom('#parseCosmeticFilters').on('change', cosmeticSwitchChanged); parseCosmeticFilters: uDom.nodeFromId('parseCosmeticFilters').checked,
uDom('#buttonApply').on('click', buttonApplyHandler); selectedLists: [],
uDom('#buttonUpdate').on('click', buttonUpdateHandler); externalLists: externalLists
uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler); };
uDom('#lists').on('change', '.listEntry > input', onListCheckboxChanged);
uDom('#lists').on('click', 'span.purge', onPurgeClicked);
uDom('#externalLists').on('input', externalListsChangeHandler);
uDom('#externalListsApply').on('click', externalListsApplyHandler);
uDom('#lists').on('click', '.groupEntry > span', groupEntryClickHandler);
renderFilterLists(); var lis = uDom('#lists .listEntry'), li;
renderExternalLists(); var i = lis.length;
}); while ( i-- ) {
li = lis.at(i);
if ( li.descendants('input').prop('checked') ) {
bin.selectedLists.push(li.descendants('a').attr('data-listkey'));
}
}
return bin;
};
var setCloudData = function(data) {
if ( typeof data !== 'object' || data === null ) {
return;
}
var checked = data.parseCosmeticFilters === true;
uDom.nodeFromId('parseCosmeticFilters').checked = checked;
listDetails.cosmetic = checked;
var lis = uDom('#lists .listEntry'), li, input, listKey;
var i = lis.length;
while ( i-- ) {
li = lis.at(i);
input = li.descendants('input');
listKey = li.descendants('a').attr('data-listkey');
checked = data.selectedLists.indexOf(listKey) !== -1;
input.prop('checked', checked);
listDetails.available[listKey].off = !checked;
}
uDom.nodeFromId('externalLists').value = data.externalLists || '';
renderWidgets();
externalListsChangeHandler();
};
self.cloud.onPush = getCloudData;
self.cloud.onPull = setCloudData;
/******************************************************************************/
uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
uDom('#parseCosmeticFilters').on('change', cosmeticSwitchChanged);
uDom('#buttonApply').on('click', buttonApplyHandler);
uDom('#buttonUpdate').on('click', buttonUpdateHandler);
uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
uDom('#lists').on('change', '.listEntry > input', onListCheckboxChanged);
uDom('#lists').on('click', 'span.purge', onPurgeClicked);
uDom('#externalLists').on('input', externalListsChangeHandler);
uDom('#externalListsApply').on('click', externalListsApplyHandler);
uDom('#lists').on('click', '.groupEntry > span', groupEntryClickHandler);
renderFilterLists();
renderExternalLists();
/******************************************************************************/ /******************************************************************************/

View file

@ -234,21 +234,39 @@ var editCancelHandler = function() {
/******************************************************************************/ /******************************************************************************/
uDom.onLoad(function() { var getCloudData = function() {
// Handle user interaction return rulesFromHTML('#diff .left li');
uDom('#importButton').on('click', startImportFilePicker); };
uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportButton').on('click', exportUserRulesToFile);
uDom('#revertButton').on('click', revertHandler); var setCloudData = function(data) {
uDom('#commitButton').on('click', commitHandler); if ( typeof data !== 'string' ) {
uDom('#editEnterButton').on('click', editStartHandler); return;
uDom('#diff > .pane.right > .rulesContainer').on('dblclick', editStartHandler); }
uDom('#editStopButton').on('click', editStopHandler); var request = {
uDom('#editCancelButton').on('click', editCancelHandler); 'what': 'setSessionRules',
'rules': data
};
messager.send(request, renderRules);
};
messager.send({ what: 'getRules' }, renderRules); self.cloud.onPush = getCloudData;
}); self.cloud.onPull = setCloudData;
/******************************************************************************/
// Handle user interaction
uDom('#importButton').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportButton').on('click', exportUserRulesToFile);
uDom('#revertButton').on('click', revertHandler);
uDom('#commitButton').on('click', commitHandler);
uDom('#editEnterButton').on('click', editStartHandler);
uDom('#diff > .pane.right > .rulesContainer').on('dblclick', editStartHandler);
uDom('#editStopButton').on('click', editStopHandler);
uDom('#editCancelButton').on('click', editCancelHandler);
messager.send({ what: 'getRules' }, renderRules);
/******************************************************************************/ /******************************************************************************/

View file

@ -33,28 +33,31 @@
/******************************************************************************/ /******************************************************************************/
// Helper to deal with the i18n'ing of HTML files. // Helper to deal with the i18n'ing of HTML files.
vAPI.i18n.render = function(context) {
uDom('[data-i18n]').forEach(function(elem) { uDom('[data-i18n]', context).forEach(function(elem) {
elem.html(vAPI.i18n(elem.attr('data-i18n'))); elem.html(vAPI.i18n(elem.attr('data-i18n')));
}); });
uDom('[title]').forEach(function(elem) { uDom('[title]', context).forEach(function(elem) {
var title = vAPI.i18n(elem.attr('title')); var title = vAPI.i18n(elem.attr('title'));
if ( title ) { if ( title ) {
elem.attr('title', title); elem.attr('title', title);
} }
}); });
uDom('[placeholder]').forEach(function(elem) { uDom('[placeholder]', context).forEach(function(elem) {
elem.attr('placeholder', vAPI.i18n(elem.attr('placeholder'))); elem.attr('placeholder', vAPI.i18n(elem.attr('placeholder')));
}); });
uDom('[data-i18n-tip]').forEach(function(elem) { uDom('[data-i18n-tip]', context).forEach(function(elem) {
elem.attr( elem.attr(
'data-tip', 'data-tip',
vAPI.i18n(elem.attr('data-i18n-tip')).replace(/<br>/g, '\n').replace(/\n{3,}/g, '\n\n') vAPI.i18n(elem.attr('data-i18n-tip')).replace(/<br>/g, '\n').replace(/\n{3,}/g, '\n\n')
); );
}); });
};
vAPI.i18n.render();
/******************************************************************************/ /******************************************************************************/

View file

@ -671,6 +671,68 @@ vAPI.messaging.listen('element-picker.js', onMessage);
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
// cloud-ui.js
(function() {
'use strict';
/******************************************************************************/
var µb = µBlock;
/******************************************************************************/
var onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
case 'cloudGetOptions':
vAPI.cloud.getOptions(function(options) {
options.enabled = µb.userSettings.cloudStorageEnabled === true;
callback(options);
});
return;
case 'cloudSetOptions':
vAPI.cloud.setOptions(request.options, callback);
return;
case 'cloudPull':
return vAPI.cloud.pull(request.datakey, callback);
case 'cloudPush':
return vAPI.cloud.push(request.datakey, request.data, callback);
default:
break;
}
// Sync
var response;
switch ( request.what ) {
// For when cloud storage is disabled.
case 'cloudPull':
// fallthrough
case 'cloudPush':
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('cloud-ui.js', onMessage);
/******************************************************************************/
})();
/******************************************************************************/
/******************************************************************************/
// 3p-filters.js // 3p-filters.js
(function() { (function() {

View file

@ -186,6 +186,12 @@ var onUserSettingsReceived = function(details) {
changeUserSettings('colorBlindFriendly', this.checked); changeUserSettings('colorBlindFriendly', this.checked);
}); });
uDom('#cloud-storage-enabled')
.prop('checked', details.cloudStorageEnabled === true)
.on('change', function(){
changeUserSettings('cloudStorageEnabled', this.checked);
});
uDom('#advanced-user-enabled') uDom('#advanced-user-enabled')
.prop('checked', details.advancedUserEnabled === true) .prop('checked', details.advancedUserEnabled === true)
.on('change', function(){ .on('change', function(){

View file

@ -66,6 +66,13 @@ var onAllReady = function() {
// for launch time. // for launch time.
µb.assets.remoteFetchBarrier -= 1; µb.assets.remoteFetchBarrier -= 1;
vAPI.cloud.start([
'tpFiltersPane',
'myFiltersPane',
'myRulesPane',
'whitelistPane'
]);
//quickProfiler.stop(0); //quickProfiler.stop(0);
vAPI.onLoadAllCompleted(); vAPI.onLoadAllCompleted();

View file

@ -96,10 +96,7 @@
/******************************************************************************/ /******************************************************************************/
µBlock.saveWhitelist = function() { µBlock.saveWhitelist = function() {
var bin = { this.keyvalSetOne('netWhitelist', this.stringFromWhitelist(this.netWhitelist));
'netWhitelist': this.stringFromWhitelist(this.netWhitelist)
};
vAPI.storage.set(bin);
this.netWhitelistModifyTime = Date.now(); this.netWhitelistModifyTime = Date.now();
}; };
@ -156,7 +153,7 @@
/******************************************************************************/ /******************************************************************************/
µBlock.saveUserFilters = function(content, callback) { µBlock.saveUserFilters = function(content, callback) {
return this.assets.put(this.userFiltersPath, content, callback); this.assets.put(this.userFiltersPath, content, callback);
}; };
/******************************************************************************/ /******************************************************************************/

View file

@ -41,7 +41,7 @@ var reUnwantedChars = /[\x00-\x09\x0b\x0c\x0e-\x1f!"$'()<>{}|\\^\[\]`~]/;
/******************************************************************************/ /******************************************************************************/
var whitelistChanged = function() { var whitelistChanged = function() {
var s = uDom('#whitelist').val().trim(); var s = uDom.nodeFromId('whitelist').value.trim();
var bad = reUnwantedChars.test(s); var bad = reUnwantedChars.test(s);
uDom('#whitelistApply').prop( uDom('#whitelistApply').prop(
'disabled', 'disabled',
@ -55,7 +55,7 @@ var whitelistChanged = function() {
var renderWhitelist = function() { var renderWhitelist = function() {
var onRead = function(whitelist) { var onRead = function(whitelist) {
cachedWhitelist = whitelist; cachedWhitelist = whitelist;
uDom('#whitelist').val(cachedWhitelist); uDom.nodeFromId('whitelist').value = whitelist;
}; };
messager.send({ what: 'getWhitelist' }, onRead); messager.send({ what: 'getWhitelist' }, onRead);
whitelistChanged(); whitelistChanged();
@ -122,15 +122,30 @@ var whitelistApplyHandler = function() {
/******************************************************************************/ /******************************************************************************/
uDom.onLoad(function() { var getCloudData = function() {
uDom('#importWhitelistFromFile').on('click', startImportFilePicker); return uDom.nodeFromId('whitelist').value;
uDom('#importFilePicker').on('change', handleImportFilePicker); };
uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile);
uDom('#whitelist').on('input', whitelistChanged);
uDom('#whitelistApply').on('click', whitelistApplyHandler);
renderWhitelist(); var setCloudData = function(data) {
}); if ( typeof data !== 'string' ) {
return;
}
uDom.nodeFromId('whitelist').value = data;
whitelistChanged();
};
self.cloud.onPush = getCloudData;
self.cloud.onPull = setCloudData;
/******************************************************************************/
uDom('#importWhitelistFromFile').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile);
uDom('#whitelist').on('input', whitelistChanged);
uDom('#whitelistApply').on('click', whitelistApplyHandler);
renderWhitelist();
/******************************************************************************/ /******************************************************************************/

View file

@ -15,6 +15,7 @@
<li><input id="icon-badge" type="checkbox"><label data-i18n="settingsIconBadgePrompt" for="icon-badge"></label> <li><input id="icon-badge" type="checkbox"><label data-i18n="settingsIconBadgePrompt" for="icon-badge"></label>
<li><input id="context-menu-enabled" type="checkbox"><label data-i18n="settingsContextMenuPrompt" for="context-menu-enabled"></label> <li><input id="context-menu-enabled" type="checkbox"><label data-i18n="settingsContextMenuPrompt" for="context-menu-enabled"></label>
<li><input id="color-blind-friendly" type="checkbox"><label data-i18n="settingsColorBlindPrompt" for="color-blind-friendly"></label> <li><input id="color-blind-friendly" type="checkbox"><label data-i18n="settingsColorBlindPrompt" for="color-blind-friendly"></label>
<li><input id="cloud-storage-enabled" type="checkbox"><label data-i18n="settingsCloudStorageEnabledPrompt" for="cloud-storage-enabled"></label>
<li><input id="advanced-user-enabled" type="checkbox"><label data-i18n="settingsAdvancedUserPrompt" for="advanced-user-enabled"></label> <li><input id="advanced-user-enabled" type="checkbox"><label data-i18n="settingsAdvancedUserPrompt" for="advanced-user-enabled"></label>
<li class="subgroup"><span data-i18n="3pGroupPrivacy"></span><ul> <li class="subgroup"><span data-i18n="3pGroupPrivacy"></span><ul>
<li><input id="prefetching-disabled" type="checkbox"><label data-i18n="settingsPrefetchingDisabledPrompt" for="prefetching-disabled"></label> <a class="fa info" href="https://wikipedia.org/wiki/Link_prefetching#Issues_and_criticisms" target="_blank">&#xf05a;</a> <li><input id="prefetching-disabled" type="checkbox"><label data-i18n="settingsPrefetchingDisabledPrompt" for="prefetching-disabled"></label> <a class="fa info" href="https://wikipedia.org/wiki/Link_prefetching#Issues_and_criticisms" target="_blank">&#xf05a;</a>

View file

@ -5,11 +5,14 @@
<title>uBlock — Whitelist</title> <title>uBlock — Whitelist</title>
<link rel="stylesheet" type="text/css" href="css/common.css"> <link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css"> <link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/cloud-ui.css">
<link rel="stylesheet" type="text/css" href="css/whitelist.css"> <link rel="stylesheet" type="text/css" href="css/whitelist.css">
</head> </head>
<body> <body>
<div id="cloudWidget" class="hide" data-cloud-entry="whitelistPane"></div>
<p data-i18n="whitelistPrompt"></p> <p data-i18n="whitelistPrompt"></p>
<p><button id="whitelistApply" class="important" type="button" disabled="true" data-i18n="whitelistApply"></button></p> <p><button id="whitelistApply" class="important" type="button" disabled="true" data-i18n="whitelistApply"></button></p>
<textarea id="whitelist" dir="auto" spellcheck="false"></textarea> <textarea id="whitelist" dir="auto" spellcheck="false"></textarea>
@ -23,6 +26,7 @@
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/whitelist.js"></script> <script src="js/whitelist.js"></script>
</body> </body>