mirror of
https://github.com/gorhill/uBlock.git
synced 2024-09-04 10:09:38 +02:00
"advanced user" flag + rearranging a bit dashboard
This commit is contained in:
parent
3a5c2cfa57
commit
881e8111d8
@ -215,6 +215,10 @@
|
||||
"message":"Make use of context menu where appropriate",
|
||||
"description":"English: Make use of context menu where appropriate"
|
||||
},
|
||||
"settingsAdvancedUserPrompt":{
|
||||
"message":"I am an advanced user (<a href='https:\/\/github.com\/gorhill\/uBlock\/wiki\/Advanced-user-features'>About<\/a>)",
|
||||
"description":"English: "
|
||||
},
|
||||
"settingsExperimentalPrompt":{
|
||||
"message":"Enable experimental features (<a href='https:\/\/github.com\/gorhill\/uBlock\/wiki\/Experimental-features'>About<\/a>)",
|
||||
"description":"English: Enable experimental features"
|
||||
|
@ -22,16 +22,6 @@
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<div style="margin:3em 0;border-top:1px solid #ccc;"></div>
|
||||
|
||||
<div style="margin:1em 0 0 0;">
|
||||
<p><button type="button" id="export" data-i18n="aboutBackupDataButton"></button>
|
||||
<button type="button" id="import" data-i18n="aboutRestoreDataButton"></button>
|
||||
<input id="restoreFilePicker" type="file" accept="text/plain" class="hiddenFileInput">
|
||||
<p>
|
||||
<p><button type="button" id="reset" data-i18n="aboutResetDataButton"></button>
|
||||
</div>
|
||||
|
||||
<script src="js/vapi-common.js"></script>
|
||||
<script src="js/vapi-client.js"></script>
|
||||
<script src="js/udom.js"></script>
|
||||
|
@ -18,14 +18,14 @@
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
[data-tip]:hover:after {
|
||||
body:not(.advancedUser) [data-tip]:hover:after {
|
||||
background-color: #ffffee;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 1px 3px gray;
|
||||
color: black;
|
||||
content: attr(data-tip);
|
||||
font: 11px sans-serif;
|
||||
font: 12px sans-serif;
|
||||
left: 0;
|
||||
line-height: 130%;
|
||||
min-width: 55%;
|
||||
|
@ -62,6 +62,7 @@ body[dir="rtl"] #panes > div:nth-of-type(2) {
|
||||
}
|
||||
|
||||
#panes > div:nth-of-type(1) {
|
||||
min-width: 150px;
|
||||
padding: 4px 5px 0 5px;
|
||||
}
|
||||
p {
|
||||
@ -115,43 +116,17 @@ p {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
#dfToggler::before {
|
||||
body.advancedUser #dfToggler::before {
|
||||
color: gray;
|
||||
content: '+\202F';
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
#panes.dfEnabled #dfToggler::before {
|
||||
body.advancedUser #panes.dfEnabled #dfToggler::before {
|
||||
content: '\2212\202F';
|
||||
}
|
||||
|
||||
.dynamicFiltering div > .tip {
|
||||
background-color: #fffff4;
|
||||
border: 1px solid #888;
|
||||
border-radius: 5px;
|
||||
bottom: 20%;
|
||||
box-shadow: 1px 1px 2px 0 rgba(0,0,0,0.8);
|
||||
display: none;
|
||||
font-size: small;
|
||||
left: 8%;
|
||||
padding: 0.25em;
|
||||
position: fixed;
|
||||
right: 8%;
|
||||
text-align: center;
|
||||
}
|
||||
.dynamicFiltering div:not(.blocked):hover > .tip:nth-of-type(1) {
|
||||
display: block;
|
||||
}
|
||||
.dynamicFiltering div.blocked:hover > .tip:nth-of-type(2) {
|
||||
display: block;
|
||||
}
|
||||
.dynamicFiltering div > .tip code {
|
||||
background-color: #f8f8f8;
|
||||
font: normal monospace;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
#dynamicFilteringContainer {
|
||||
border: 0;
|
||||
font-size: 12px;
|
||||
@ -269,17 +244,3 @@ p {
|
||||
#dynamicFilteringContainer span.bRule #actionSelector > span:nth-of-type(3) {
|
||||
visibility: hidden;
|
||||
}
|
||||
#hotspotTip {
|
||||
background-color: #ffe;
|
||||
border: 1px dotted #ddb;
|
||||
border-radius: 5px;
|
||||
height: 50vh;
|
||||
opacity: 1;
|
||||
padding: 4px;
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
text-align: center;
|
||||
top: 25vh;
|
||||
width: 20vw;
|
||||
z-index: 100;
|
||||
}
|
@ -11,11 +11,11 @@
|
||||
<div id="dashboard-nav">
|
||||
<div id="dashboard-nav-widgets">
|
||||
<span data-i18n="extName"></span>
|
||||
<a class="tabButton" href="#settings.html" data-i18n="settingsPageName"></a>
|
||||
<a class="tabButton" href="#3p-filters.html" data-i18n="3pPageName"></a>
|
||||
<a class="tabButton" href="#1p-filters.html" data-i18n="1pPageName"></a>
|
||||
<a class="tabButton" href="#dyna-rules.html" data-i18n="rulesPageName"></a>
|
||||
<a class="tabButton" href="#whitelist.html" data-i18n="whitelistPageName"></a>
|
||||
<a class="tabButton" href="#settings.html" data-i18n="settingsPageName"></a>
|
||||
<a class="tabButton" href="#about.html" data-i18n="aboutPageName"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,101 +33,8 @@ var messager = vAPI.messaging.channel('about.js');
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var exportToFile = function() {
|
||||
var onUserDataReady = function(userData) {
|
||||
if (!userData) {
|
||||
return;
|
||||
}
|
||||
var now = new Date();
|
||||
var filename = vAPI.i18n('aboutBackupFilename')
|
||||
.replace('{{datetime}}', now.toLocaleString())
|
||||
.replace(/ +/g, '_');
|
||||
vAPI.download({
|
||||
'url': 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(userData, null, ' ')),
|
||||
'filename': filename
|
||||
});
|
||||
};
|
||||
|
||||
messager.send({ what: 'getUserData' }, onUserDataReady);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var handleImportFilePicker = function() {
|
||||
var fileReaderOnLoadHandler = function() {
|
||||
var userData;
|
||||
try {
|
||||
userData = JSON.parse(this.result);
|
||||
if ( typeof userData !== 'object' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
if ( typeof userData.userSettings !== 'object' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
if ( typeof userData.netWhitelist !== 'string' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
if ( typeof userData.filterLists !== 'object' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
userData = undefined;
|
||||
}
|
||||
if ( userData === undefined ) {
|
||||
window.alert(vAPI.i18n('aboutRestoreDataError'));
|
||||
return;
|
||||
}
|
||||
var time = new Date(userData.timeStamp);
|
||||
var msg = vAPI.i18n('aboutRestoreDataConfirm')
|
||||
.replace('{{time}}', time.toLocaleString());
|
||||
var proceed = window.confirm(msg);
|
||||
if ( proceed ) {
|
||||
messager.send({ what: 'restoreUserData', userData: userData });
|
||||
}
|
||||
};
|
||||
|
||||
var file = this.files[0];
|
||||
if ( file === undefined || file.name === '' ) {
|
||||
return;
|
||||
}
|
||||
if ( file.type.indexOf('text') !== 0 ) {
|
||||
return;
|
||||
}
|
||||
var fr = new FileReader();
|
||||
fr.onload = fileReaderOnLoadHandler;
|
||||
fr.readAsText(file);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var startImportFilePicker = function() {
|
||||
var input = document.getElementById('restoreFilePicker');
|
||||
// Reset to empty string, this will ensure an change event is properly
|
||||
// triggered if the user pick a file, even if it is the same as the last
|
||||
// one picked.
|
||||
input.value = '';
|
||||
input.click();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var resetUserData = function() {
|
||||
var msg = vAPI.i18n('aboutResetDataConfirm');
|
||||
var proceed = window.confirm(msg);
|
||||
if ( proceed ) {
|
||||
messager.send({ what: 'resetUserData' });
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var onAppDataReady = function(appData) {
|
||||
uDom('#aboutNameVer').html(appData.name + ' v' + appData.version);
|
||||
uDom('#export').on('click', exportToFile);
|
||||
uDom('#import').on('click', startImportFilePicker);
|
||||
uDom('#reset').on('click', resetUserData);
|
||||
uDom('#restoreFilePicker').on('change', handleImportFilePicker);
|
||||
};
|
||||
|
||||
messager.send({ what: 'getAppData' }, onAppDataReady);
|
||||
|
@ -52,6 +52,7 @@ var defaultExternalLists = [
|
||||
|
||||
return {
|
||||
userSettings: {
|
||||
advancedUserEnabled: false,
|
||||
autoUpdate: true,
|
||||
collapseBlocked: true,
|
||||
contextMenuEnabled: true,
|
||||
|
@ -59,7 +59,7 @@ uDom.onLoad(function() {
|
||||
}
|
||||
}
|
||||
if ( !tab ) {
|
||||
tab = '3p-filters';
|
||||
tab = 'settings';
|
||||
}
|
||||
loadDashboardPanel(tab + '.html', q);
|
||||
uDom('.tabButton').on('click', onTabClickHandler);
|
||||
|
@ -171,17 +171,18 @@ var getDynamicFilterRules = function(srcHostname, desHostnames) {
|
||||
|
||||
var getStats = function(tabId) {
|
||||
var r = {
|
||||
advancedUserEnabled: µb.userSettings.advancedUserEnabled,
|
||||
appName: vAPI.app.name,
|
||||
appVersion: vAPI.app.version,
|
||||
globalBlockedRequestCount: µb.localSettings.blockedRequestCount,
|
||||
globalAllowedRequestCount: µb.localSettings.allowedRequestCount,
|
||||
tabId: tabId,
|
||||
pageURL: '',
|
||||
pageBlockedRequestCount: 0,
|
||||
pageAllowedRequestCount: 0,
|
||||
netFilteringSwitch: false,
|
||||
cosmeticFilteringSwitch: false,
|
||||
dfEnabled: µb.userSettings.dynamicFilteringEnabled
|
||||
dfEnabled: µb.userSettings.dynamicFilteringEnabled,
|
||||
globalAllowedRequestCount: µb.localSettings.allowedRequestCount,
|
||||
globalBlockedRequestCount: µb.localSettings.blockedRequestCount,
|
||||
netFilteringSwitch: false,
|
||||
pageURL: '',
|
||||
pageAllowedRequestCount: 0,
|
||||
pageBlockedRequestCount: 0,
|
||||
tabId: tabId
|
||||
};
|
||||
var pageStore = µb.pageStoreFromTabId(tabId);
|
||||
if ( pageStore ) {
|
||||
@ -811,7 +812,7 @@ vAPI.messaging.listen('stats.js', onMessage);
|
||||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
|
||||
// about.js
|
||||
// settings.js
|
||||
|
||||
(function() {
|
||||
|
||||
@ -902,7 +903,7 @@ var onMessage = function(request, sender, callback) {
|
||||
callback(response);
|
||||
};
|
||||
|
||||
vAPI.messaging.listen('about.js', onMessage);
|
||||
vAPI.messaging.listen('settings.js', onMessage);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
@ -615,10 +615,12 @@ PageStore.prototype.filterRequest = function(context) {
|
||||
// - Evaluating dynamic filtering is much faster than static filtering
|
||||
// We evaluate dynamic filtering first, and hopefully we can skip
|
||||
// evaluation of static filtering.
|
||||
var df = µb.dynamicNetFilteringEngine.clearRegisters();
|
||||
df.evaluateCellZY(context.rootHostname, context.requestHostname, context.requestType);
|
||||
if ( df.mustBlockOrAllow() ) {
|
||||
result = df.toFilterString();
|
||||
if ( µb.userSettings.advancedUserEnabled ) {
|
||||
var df = µb.dynamicNetFilteringEngine.clearRegisters();
|
||||
df.evaluateCellZY(context.rootHostname, context.requestHostname, context.requestType);
|
||||
if ( df.mustBlockOrAllow() ) {
|
||||
result = df.toFilterString();
|
||||
}
|
||||
}
|
||||
|
||||
// Static filtering never override dynamic filtering
|
||||
|
@ -117,7 +117,7 @@ var reRulekeyCompareNoise = /[^a-z0-9.]/g;
|
||||
var addDynamicFilterRow = function(des) {
|
||||
var row = uDom('#templates > div:nth-of-type(1)').clone();
|
||||
row.descendants('[data-des]').attr('data-des', des);
|
||||
row.descendants('div > span:nth-of-type(1)').text(des);
|
||||
row.descendants('span:nth-of-type(1)').text(des);
|
||||
|
||||
var hnDetails = popupData.hostnameDict[des] || {};
|
||||
var isDomain = des === hnDetails.domain;
|
||||
@ -245,16 +245,17 @@ var renderPopup = function() {
|
||||
|
||||
var isHTTP = /^https?:\/\/[0-9a-z]/.test(popupData.pageURL);
|
||||
|
||||
// Condition for dynamic filtering toggler:
|
||||
// - Advanced user
|
||||
uDom('body').toggleClass('advancedUser', popupData.advancedUserEnabled);
|
||||
|
||||
// Conditions for request log:
|
||||
// - `http` or `https` scheme
|
||||
// - `http` or `https` scheme
|
||||
uDom('#gotoLog').toggleClass('enabled', isHTTP);
|
||||
|
||||
// Conditions for element picker:
|
||||
// - `http` or `https` scheme
|
||||
uDom('#gotoPick').toggleClass(
|
||||
'enabled',
|
||||
isHTTP
|
||||
);
|
||||
// - `http` or `https` scheme
|
||||
uDom('#gotoPick').toggleClass('enabled', isHTTP);
|
||||
|
||||
var or = vAPI.i18n('popupOr');
|
||||
var blocked = popupData.pageBlockedRequestCount;
|
||||
@ -289,15 +290,15 @@ var renderPopup = function() {
|
||||
'%</span>'
|
||||
);
|
||||
}
|
||||
uDom('#total-blocked').html(html.join(''));
|
||||
|
||||
// Build dynamic filtering pane only if in use
|
||||
if ( popupData.dfEnabled ) {
|
||||
if ( popupData.dfEnabled && popupData.advancedUserEnabled ) {
|
||||
syncAllDynamicFilters();
|
||||
}
|
||||
|
||||
uDom('#total-blocked').html(html.join(''));
|
||||
uDom('#switch .fa').toggleClass('off', popupData.pageURL === '' || !popupData.netFilteringSwitch);
|
||||
uDom('#panes').toggleClass('dfEnabled', popupData.dfEnabled);
|
||||
uDom('#panes').toggleClass('dfEnabled', popupData.dfEnabled && popupData.advancedUserEnabled);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
@ -373,6 +374,9 @@ var gotoLink = function(ev) {
|
||||
/******************************************************************************/
|
||||
|
||||
var toggleDynamicFiltering = function(ev) {
|
||||
if ( uDom('body').hasClass('advancedUser') === false ) {
|
||||
return;
|
||||
}
|
||||
var el = uDom('#panes');
|
||||
popupData.dfEnabled = !popupData.dfEnabled;
|
||||
messager.send({
|
||||
|
@ -20,15 +20,105 @@
|
||||
*/
|
||||
|
||||
/* global vAPI, uDom */
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
uDom.onLoad(function() {
|
||||
var messager = vAPI.messaging.channel('settings.js');
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var messager = vAPI.messaging.channel('settings.js');
|
||||
var exportToFile = function() {
|
||||
var onUserDataReady = function(userData) {
|
||||
if (!userData) {
|
||||
return;
|
||||
}
|
||||
var now = new Date();
|
||||
var filename = vAPI.i18n('aboutBackupFilename')
|
||||
.replace('{{datetime}}', now.toLocaleString())
|
||||
.replace(/ +/g, '_');
|
||||
vAPI.download({
|
||||
'url': 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(userData, null, ' ')),
|
||||
'filename': filename
|
||||
});
|
||||
};
|
||||
|
||||
messager.send({ what: 'getUserData' }, onUserDataReady);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var handleImportFilePicker = function() {
|
||||
var fileReaderOnLoadHandler = function() {
|
||||
var userData;
|
||||
try {
|
||||
userData = JSON.parse(this.result);
|
||||
if ( typeof userData !== 'object' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
if ( typeof userData.userSettings !== 'object' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
if ( typeof userData.netWhitelist !== 'string' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
if ( typeof userData.filterLists !== 'object' ) {
|
||||
throw 'Invalid';
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
userData = undefined;
|
||||
}
|
||||
if ( userData === undefined ) {
|
||||
window.alert(vAPI.i18n('aboutRestoreDataError'));
|
||||
return;
|
||||
}
|
||||
var time = new Date(userData.timeStamp);
|
||||
var msg = vAPI.i18n('aboutRestoreDataConfirm')
|
||||
.replace('{{time}}', time.toLocaleString());
|
||||
var proceed = window.confirm(msg);
|
||||
if ( proceed ) {
|
||||
messager.send({ what: 'restoreUserData', userData: userData });
|
||||
}
|
||||
};
|
||||
|
||||
var file = this.files[0];
|
||||
if ( file === undefined || file.name === '' ) {
|
||||
return;
|
||||
}
|
||||
if ( file.type.indexOf('text') !== 0 ) {
|
||||
return;
|
||||
}
|
||||
var fr = new FileReader();
|
||||
fr.onload = fileReaderOnLoadHandler;
|
||||
fr.readAsText(file);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var startImportFilePicker = function() {
|
||||
var input = document.getElementById('restoreFilePicker');
|
||||
// Reset to empty string, this will ensure an change event is properly
|
||||
// triggered if the user pick a file, even if it is the same as the last
|
||||
// one picked.
|
||||
input.value = '';
|
||||
input.click();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
var resetUserData = function() {
|
||||
var msg = vAPI.i18n('aboutResetDataConfirm');
|
||||
var proceed = window.confirm(msg);
|
||||
if ( proceed ) {
|
||||
messager.send({ what: 'resetUserData' });
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
@ -63,15 +153,30 @@ var onUserSettingsReceived = function(details) {
|
||||
changeUserSettings('contextMenuEnabled', this.checked);
|
||||
});
|
||||
|
||||
uDom('#advanced-user-enabled')
|
||||
.prop('checked', details.advancedUserEnabled === true)
|
||||
.on('change', function(){
|
||||
changeUserSettings('advancedUserEnabled', this.checked);
|
||||
});
|
||||
|
||||
uDom('#experimental-enabled')
|
||||
.prop('checked', details.experimentalEnabled === true)
|
||||
.on('change', function(){
|
||||
changeUserSettings('experimentalEnabled', this.checked);
|
||||
});
|
||||
};
|
||||
|
||||
messager.send({ what: 'userSettings' }, onUserSettingsReceived);
|
||||
uDom('#export').on('click', exportToFile);
|
||||
uDom('#import').on('click', startImportFilePicker);
|
||||
uDom('#reset').on('click', resetUserData);
|
||||
uDom('#restoreFilePicker').on('change', handleImportFilePicker);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
uDom.onLoad(function() {
|
||||
messager.send({ what: 'userSettings' }, onUserSettingsReceived);
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
@ -670,11 +670,14 @@
|
||||
µb.dynamicNetFilteringEngine.fromObsoleteSelfie(settings.dynamicFilteringSelfie);
|
||||
µb.userSettings.dynamicFilteringString = µb.dynamicNetFilteringEngine.toString();
|
||||
µb.XAL.keyvalSetOne('dynamicFilteringString', µb.userSettings.dynamicFilteringString);
|
||||
|
||||
// Auto-enable advanced user if there were dynamic rules
|
||||
µb.userSettings.advancedUserEnabled = true;
|
||||
µb.XAL.keyvalSetOne('advancedUserEnabled', true);
|
||||
}
|
||||
delete µb.userSettings.dynamicFilteringSelfie;
|
||||
µb.XAL.keyvalRemoveOne('dynamicFilteringSelfie');
|
||||
}
|
||||
µb.dynamicNetFilteringEngine.fromString(µb.userSettings.dynamicFilteringString);
|
||||
};
|
||||
|
||||
this.loadUserSettings(onUserSettingsReady);
|
||||
|
@ -8,6 +8,7 @@
|
||||
<style>
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 1em;
|
||||
}
|
||||
#experimental-enabled {
|
||||
margin-top: 1em;
|
||||
@ -21,9 +22,21 @@ ul {
|
||||
<li><input id="collapse-blocked" type="checkbox"><label data-i18n="settingsCollapseBlockedPrompt" for="collapse-blocked"></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="advanced-user-enabled" type="checkbox"><label data-i18n="settingsAdvancedUserPrompt" for="advanced-user-enabled"></label>
|
||||
|
||||
<li><input id="experimental-enabled" type="checkbox"><label data-i18n="settingsExperimentalPrompt" for="experimental-enabled"></label>
|
||||
</ul>
|
||||
|
||||
<div style="margin:3em 0;border-top:1px solid #ccc;"></div>
|
||||
|
||||
<div style="margin:1em 0 0 1em;">
|
||||
<p><button type="button" id="export" data-i18n="aboutBackupDataButton"></button> 
|
||||
<button type="button" id="import" data-i18n="aboutRestoreDataButton"></button>
|
||||
<input id="restoreFilePicker" type="file" accept="text/plain" class="hiddenFileInput">
|
||||
<p>
|
||||
<p><button type="button" id="reset" data-i18n="aboutResetDataButton"></button>
|
||||
</div>
|
||||
|
||||
<script src="js/vapi-common.js"></script>
|
||||
<script src="js/vapi-client.js"></script>
|
||||
<script src="js/udom.js"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user