diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 867587b..b007b4b 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -551,6 +551,14 @@
"message": "Import...",
"description": ""
},
+ "assetsInlineHostsLabel": {
+ "message": "My hosts",
+ "description": ""
+ },
+ "assetsInlineRecipesLabel": {
+ "message": "My recipes",
+ "description": ""
+ },
"rawSettingsWarning" : {
"message": "Warning! Change these raw configuration settings at your own risk.",
diff --git a/src/css/hosts-files.css b/src/css/hosts-files.css
index 2218944..1aa982d 100644
--- a/src/css/hosts-files.css
+++ b/src/css/hosts-files.css
@@ -139,20 +139,29 @@ body.updating li.listEntry.obsolete > input[type="checkbox"]:checked ~ span.upda
.dim {
opacity: 0.5;
}
-li.listEntry.importURL > input[type="checkbox"] ~ textarea {
+li.listEntry.notAnAsset > input[type="checkbox"] ~ textarea {
display: none;
margin-left: 1.6em;
}
-li.listEntry.importURL > input[type="checkbox"]:checked ~ textarea {
+li.listEntry.notAnAsset > input[type="checkbox"]:checked ~ textarea {
display: block;
}
-li.listEntry.importURL > textarea {
+li.listEntry.notAnAsset > textarea {
border: 1px solid #ddd;
box-sizing: border-box;
display: block;
font-size: smaller;
width: calc(100% - 4em);
height: 6em;
- resize: none;
+ resize: vertical;
white-space: pre;
}
+#recipes li.listEntry.toInline {
+ display: none;
+ }
+body.contributor #recipes li.listEntry.toInline {
+ display: block;
+ }
+#recipes li.listEntry.toInline > textarea {
+ height: 18em;
+ }
diff --git a/src/hosts-files.html b/src/hosts-files.html
index f07386d..61c0af7 100644
--- a/src/hosts-files.html
+++ b/src/hosts-files.html
@@ -24,7 +24,7 @@
@@ -35,8 +35,10 @@
diff --git a/src/js/background.js b/src/js/background.js
index 8825702..cc5661b 100644
--- a/src/js/background.js
+++ b/src/js/background.js
@@ -101,6 +101,7 @@ var requestStatsFactory = function() {
*/
var rawSettingsDefault = {
+ contributorMode: false,
disableCSPReportInjection: false,
placeholderBackground:
[
@@ -188,7 +189,15 @@ return {
processHyperlinkAuditing: true,
processReferer: false,
selectedHostsFiles: [ '' ],
- selectedRecipeFiles: [ '' ]
+ selectedRecipeFiles: [ '' ],
+ userHosts: {
+ enabled: false,
+ content: ''
+ },
+ userRecipes: {
+ enabled: false,
+ content: ''
+ }
},
rawSettingsDefault: rawSettingsDefault,
diff --git a/src/js/hosts-files.js b/src/js/hosts-files.js
index e0f4d34..960de02 100644
--- a/src/js/hosts-files.js
+++ b/src/js/hosts-files.js
@@ -145,22 +145,23 @@ var renderHostsFiles = function(soft) {
});
let ulList = document.querySelector(listSelector),
- liImport = ulList.querySelector('.importURL');
- if ( liImport.parentNode !== null ) {
- liImport.parentNode.removeChild(liImport);
- }
+ liLast = ulList.querySelector('.notAnAsset');
+
for ( let i = 0; i < assetKeys.length; i++ ) {
- let liEntry = liFromListEntry(
- collection,
- assetKeys[i],
- ulList.children[i]
- );
+ let liReuse = i < ulList.childElementCount ?
+ ulList.children[i] :
+ null;
+ if (
+ liReuse !== null &&
+ liReuse.classList.contains('notAnAsset')
+ ) {
+ liReuse = null;
+ }
+ let liEntry = liFromListEntry(collection, assetKeys[i], liReuse);
if ( liEntry.parentElement === null ) {
- ulList.appendChild(liEntry);
+ ulList.insertBefore(liEntry, liLast);
}
}
-
- ulList.appendChild(liImport);
};
var onAssetDataReceived = function(details) {
@@ -171,9 +172,14 @@ var renderHostsFiles = function(soft) {
// Before all, set context vars
listDetails = details;
+ document.body.classList.toggle(
+ 'contributor',
+ listDetails.contributor === true
+ );
+
// Incremental rendering: this will allow us to easily discard unused
// DOM list entries.
- uDom('#hosts .listEntry:not(.importURL)').addClass('discard');
+ uDom('#hosts .listEntry:not(.notAnAsset)').addClass('discard');
onRenderAssetFiles(details.hosts, '#hosts');
onRenderAssetFiles(details.recipes, '#recipes');
@@ -188,6 +194,12 @@ var renderHostsFiles = function(soft) {
);
uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true);
+ uDom.nodeFromSelector('#recipes .toInline > input[type="checkbox"]').checked =
+ listDetails.userRecipes.enabled === true;
+ uDom.nodeFromSelector('#recipes .toInline > textarea').value =
+ listDetails.userRecipes.content;
+
+
if ( !soft ) {
hostsFilesSettingsHash = hashFromCurrentFromSettings();
}
@@ -231,31 +243,50 @@ var updateAssetStatus = function(details) {
/*******************************************************************************
- Compute a hash from all the settings affecting how filter lists are loaded
+ Compute a hash from all the settings affecting how assets are loaded
in memory.
**/
var hashFromCurrentFromSettings = function() {
- var hash = [],
- listHash = [],
- listEntries = document.querySelectorAll('.assets .listEntry[data-listkey]:not(.toRemove)'),
- i = listEntries.length;
- while ( i-- ) {
- let liEntry = listEntries[i];
+ let listHash = [],
+ listEntries = document.querySelectorAll(
+ '.assets .listEntry[data-listkey]:not(.toRemove)'
+ );
+ for ( let liEntry of listEntries ) {
if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
listHash.push(liEntry.getAttribute('data-listkey'));
}
}
- let textarea1 = document.querySelector('.assets .importURL > input[type="checkbox"]:checked ~ textarea');
- let textarea2 = document.querySelector('#recipes .importURL > input[type="checkbox"]:checked ~ textarea');
- hash.push(
- listHash.sort().join(),
- textarea1 !== null && reValidExternalList.test(textarea1.value),
- textarea2 !== null && reValidExternalList.test(textarea2.value),
- document.querySelector('.listEntry.toRemove') !== null
- );
- return hash.join();
+ return [
+ listHash.join(),
+ document.querySelector('.listEntry.toRemove') !== null,
+ reValidExternalList.test(
+ textFromTextarea(
+ '#hosts .toImport > input[type="checkbox"]:checked ~ textarea'
+ )
+ ),
+ textFromTextarea(
+ '#hosts .toInline > input[type="checkbox"]:checked ~ textarea'
+ ),
+ reValidExternalList.test(
+ textFromTextarea(
+ '#recipes .toImport > input[type="checkbox"]:checked ~ textarea'
+ )
+ ),
+ textFromTextarea(
+ '#recipes .toInline > input[type="checkbox"]:checked ~ textarea'
+ ),
+ ].join('\n');
+};
+
+/******************************************************************************/
+
+var textFromTextarea = function(textarea) {
+ if ( typeof textarea === 'string' ) {
+ textarea = document.querySelector(textarea);
+ }
+ return textarea !== null ? textarea.value.trim() : '';
};
/******************************************************************************/
@@ -300,36 +331,44 @@ var selectAssets = function(callback) {
var out = {
toSelect: [],
toImport: '',
- toRemove: []
+ toRemove: [],
+ toInline: {
+ enabled: false,
+ content: ''
+ }
};
let root = document.querySelector(listSelector);
- let liEntries = root.querySelectorAll('.listEntry[data-listkey]:not(.toRemove)'),
- i = liEntries.length;
- while ( i-- ) {
- let liEntry = liEntries[i];
- if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
+ // Lists to select or remove
+ let liEntries = root.querySelectorAll(
+ '.listEntry[data-listkey]:not(.notAnAsset)'
+ );
+ for ( let liEntry of liEntries ) {
+ if ( liEntry.classList.contains('toRemove') ) {
+ out.toRemove.push(liEntry.getAttribute('data-listkey'));
+ } else if ( liEntry.querySelector('input[type="checkbox"]:checked') ) {
out.toSelect.push(liEntry.getAttribute('data-listkey'));
}
}
- // External hosts files to remove
- liEntries = root.querySelectorAll('.listEntry.toRemove[data-listkey]');
- i = liEntries.length;
- while ( i-- ) {
- out.toRemove.push(liEntries[i].getAttribute('data-listkey'));
- }
-
// External hosts files to import
- let input = root.querySelector('.importURL > input[type="checkbox"]:checked');
+ let input = root.querySelector(
+ '.toImport > input[type="checkbox"]:checked'
+ );
if ( input !== null ) {
- let textarea = root.querySelector('.importURL textarea');
+ let textarea = root.querySelector('.toImport textarea');
out.toImport = textarea.value.trim();
textarea.value = '';
input.checked = false;
}
+ // Inline data
+ out.toInline.enabled = root.querySelector(
+ '.toInline > input[type="checkbox"]:checked'
+ ) !== null;
+ out.toInline.content = textFromTextarea('.toInline > textarea');
+
return out;
};
@@ -406,7 +445,7 @@ uDom('#buttonApply').on('click', buttonApplyHandler);
uDom('#buttonUpdate').on('click', buttonUpdateHandler);
uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
uDom('.assets').on('change', '.listEntry > input', onHostsFilesSettingsChanged);
-uDom('.assets').on('input', '.listEntry.importURL textarea', onHostsFilesSettingsChanged);
+uDom('.assets').on('input', '.listEntry > textarea', onHostsFilesSettingsChanged);
uDom('.assets').on('click', '.listEntry > a.remove', onRemoveExternalAsset);
uDom('.assets').on('click', 'span.cache', onPurgeClicked);
diff --git a/src/js/messaging.js b/src/js/messaging.js
index 70a9793..6397ce3 100644
--- a/src/js/messaging.js
+++ b/src/js/messaging.js
@@ -736,7 +736,9 @@ var getAssets = function(callback) {
blockedHostnameCount: µm.ubiquitousBlacklist.count,
hosts: null,
recipes: null,
- cache: null
+ userRecipes: µm.userSettings.userRecipes,
+ cache: null,
+ contributor: µm.rawSettings.contributorMode
};
var onMetadataReady = function(entries) {
r.cache = entries;
diff --git a/src/js/storage.js b/src/js/storage.js
index 22ac5e1..3787589 100644
--- a/src/js/storage.js
+++ b/src/js/storage.js
@@ -292,6 +292,14 @@
for ( let assetKey of µm.userSettings.selectedRecipeFiles ) {
this.assets.get(assetKey, onLoaded);
}
+
+ let userRecipes = µm.userSettings.userRecipes;
+ if ( userRecipes.enabled ) {
+ µm.recipeManager.fromString(
+ '! uMatrix: Ruleset recipes 1.0\n' + userRecipes.content
+ );
+ }
+
};
/******************************************************************************/
@@ -572,7 +580,8 @@
metadata,
details,
propSelectedAssetKeys,
- propImportedAssetKeys
+ propImportedAssetKeys,
+ propInlineAsset
) {
let µmus = µm.userSettings;
let selectedAssetKeys = new Set();
@@ -629,14 +638,35 @@
}
let bin = {},
- changed = false;
-
+ needReload = false;
+
+ if ( details.toInline instanceof Object ) {
+ let newInline = details.toInline;
+ let oldInline = µmus[propInlineAsset];
+ newInline.content = newInline.content.trim();
+ if ( newInline.content.length !== 0 ) {
+ newInline.content += '\n';
+ }
+ let newContent = newInline.enabled ? newInline.content : '';
+ let oldContent = oldInline.enabled ? oldInline.content : '';
+ if ( newContent !== oldContent ) {
+ needReload = true;
+ }
+ if (
+ newInline.enabled !== oldInline.enabled ||
+ newInline.content !== oldInline.content
+ ) {
+ µmus[propInlineAsset] = newInline;
+ bin[propInlineAsset] = newInline;
+ }
+ }
+
selectedAssetKeys = Array.from(selectedAssetKeys).sort();
µmus[propSelectedAssetKeys].sort();
if ( selectedAssetKeys.join() !== µmus[propSelectedAssetKeys].join() ) {
µmus[propSelectedAssetKeys] = selectedAssetKeys;
bin[propSelectedAssetKeys] = selectedAssetKeys;
- changed = true;
+ needReload = true;
}
importedAssetKeys = Array.from(importedAssetKeys).sort();
@@ -644,14 +674,14 @@
if ( importedAssetKeys.join() !== µmus[propImportedAssetKeys].join() ) {
µmus[propImportedAssetKeys] = importedAssetKeys;
bin[propImportedAssetKeys] = importedAssetKeys;
- changed = true;
+ needReload = true;
}
- if ( changed ) {
+ if ( Object.keys(bin).length !== 0 ) {
vAPI.storage.set(bin);
}
- return changed;
+ return needReload;
};
var onMetadataReady = function(response) {
@@ -660,7 +690,8 @@
metadata,
details.hosts,
'selectedHostsFiles',
- 'externalHostsFiles'
+ 'externalHostsFiles',
+ 'userHosts'
);
if ( hostsChanged ) {
µm.hostsFilesSelfie.destroy();
@@ -669,7 +700,8 @@
metadata,
details.recipes,
'selectedRecipeFiles',
- 'externalRecipeFiles'
+ 'externalRecipeFiles',
+ 'userRecipes'
);
if ( recipesChanged ) {
µm.recipeManager.reset();