mirror of https://github.com/gorhill/uBlock.git
Offer opportunity to update filter lists before reporting issue
Related discussion: - https://github.com/uBlockOrigin/uBlock-issues/discussions/2582 If there exist any built-in filter list which last update time is older than 2 hours, the "Report a filter issue" page will ask the user to update their filter lists then verify that the issue still exists. Once filter lists are updated, the troubleshooting information will reflect the change in update time.
This commit is contained in:
parent
ec4480e122
commit
4a92f96206
|
@ -952,6 +952,14 @@
|
||||||
"message": "To avoid burdening volunteers with duplicate reports, please verify that the issue has not already been reported.",
|
"message": "To avoid burdening volunteers with duplicate reports, please verify that the issue has not already been reported.",
|
||||||
"description": "A paragraph in the filter issue reporter section"
|
"description": "A paragraph in the filter issue reporter section"
|
||||||
},
|
},
|
||||||
|
"supportS6P2S1": {
|
||||||
|
"message": "Filter lists are updated daily. Be sure your issue has not already been addressed in the most recent filter lists.",
|
||||||
|
"description": "A paragraph in the filter issue reporter section"
|
||||||
|
},
|
||||||
|
"supportS6P2S2": {
|
||||||
|
"message": "Verify that the issue still exists after reloading the problematic webpage.",
|
||||||
|
"description": "A paragraph in the filter issue reporter section"
|
||||||
|
},
|
||||||
"supportS6URL": {
|
"supportS6URL": {
|
||||||
"message": "Address of the web page:",
|
"message": "Address of the web page:",
|
||||||
"description": "Label for the URL of the page"
|
"description": "Label for the URL of the page"
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin-bottom: 6rem;
|
margin-bottom: 6rem;
|
||||||
}
|
}
|
||||||
|
@ -11,6 +16,7 @@ h3 {
|
||||||
}
|
}
|
||||||
.supportEntry {
|
.supportEntry {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-block: 1em;
|
||||||
}
|
}
|
||||||
:root.mobile .supportEntry {
|
:root.mobile .supportEntry {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -48,6 +54,30 @@ body.filterIssue #moreButton {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:not(.shouldUpdate) .shouldUpdate {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
body.updating {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
body.updating button {
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
body.updated .shouldUpdate button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
body.updating .shouldUpdate button .fa-icon svg {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
transform-origin: 50%;
|
||||||
|
}
|
||||||
|
body .shouldUpdate .updated {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
body:not(.updated) .shouldUpdate .updated {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -946,6 +946,29 @@ assets.rmrf = function() {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
assets.getUpdateAges = async function(conditions = {}) {
|
||||||
|
const assetDict = await assets.metadata();
|
||||||
|
const now = Date.now();
|
||||||
|
const out = [];
|
||||||
|
for ( const [ assetKey, asset ] of Object.entries(assetDict) ) {
|
||||||
|
if ( asset.hasRemoteURL !== true ) { continue; }
|
||||||
|
const tokens = conditions[asset.content];
|
||||||
|
if ( Array.isArray(tokens) === false ) { continue; }
|
||||||
|
if ( tokens.includes('*') === false ) {
|
||||||
|
if ( tokens.includes(assetKey) === false ) { continue; }
|
||||||
|
}
|
||||||
|
const age = now - (asset.writeTime || 0);
|
||||||
|
out.push({
|
||||||
|
assetKey,
|
||||||
|
age,
|
||||||
|
ageNormalized: age / (asset.updateAfter * 86400000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
// Asset updater area.
|
// Asset updater area.
|
||||||
const updaterAssetDelayDefault = 120000;
|
const updaterAssetDelayDefault = 120000;
|
||||||
const updaterUpdated = [];
|
const updaterUpdated = [];
|
||||||
|
|
|
@ -589,6 +589,53 @@ const getElementCount = async function(tabId, what) {
|
||||||
return total;
|
return total;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const launchReporter = async function(request) {
|
||||||
|
const pageStore = µb.pageStoreFromTabId(request.tabId);
|
||||||
|
if ( pageStore === null ) { return; }
|
||||||
|
if ( pageStore.hasUnprocessedRequest ) {
|
||||||
|
request.popupPanel.hasUnprocessedRequest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await io.getUpdateAges({
|
||||||
|
filters: µb.selectedFilterLists.filter(
|
||||||
|
a => (/^https?:/.test(a) === false)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let shoudUpdateLists = false;
|
||||||
|
for ( const entry of entries ) {
|
||||||
|
if ( entry.age < (2 * 60 * 60 * 1000) ) { continue; }
|
||||||
|
io.purge(entry.assetKey);
|
||||||
|
shoudUpdateLists = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/gorhill/uBlock/commit/6efd8eb#commitcomment-107523558
|
||||||
|
// Important: for whatever reason, not using `document_start` causes the
|
||||||
|
// Promise returned by `tabs.executeScript()` to resolve only when the
|
||||||
|
// associated tab is closed.
|
||||||
|
const cosmeticSurveyResults = await vAPI.tabs.executeScript(request.tabId, {
|
||||||
|
allFrames: true,
|
||||||
|
file: '/js/scriptlets/cosmetic-report.js',
|
||||||
|
matchAboutBlank: true,
|
||||||
|
runAt: 'document_start',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filters = cosmeticSurveyResults.reduce((a, v) => {
|
||||||
|
if ( Array.isArray(v) ) { a.push(...v); }
|
||||||
|
return a;
|
||||||
|
}, []);
|
||||||
|
if ( filters.length !== 0 ) {
|
||||||
|
request.popupPanel.cosmetic = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportURL = new URL(vAPI.getURL('support.html'));
|
||||||
|
supportURL.searchParams.set('pageURL', request.pageURL);
|
||||||
|
supportURL.searchParams.set('popupPanel', JSON.stringify(request.popupPanel));
|
||||||
|
if ( shoudUpdateLists ) {
|
||||||
|
supportURL.searchParams.set('shouldUpdate', 1);
|
||||||
|
}
|
||||||
|
return supportURL.href;
|
||||||
|
};
|
||||||
|
|
||||||
const onMessage = function(request, sender, callback) {
|
const onMessage = function(request, sender, callback) {
|
||||||
// Async
|
// Async
|
||||||
switch ( request.what ) {
|
switch ( request.what ) {
|
||||||
|
@ -610,36 +657,6 @@ const onMessage = function(request, sender, callback) {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// https://github.com/gorhill/uBlock/commit/6efd8eb#commitcomment-107523558
|
|
||||||
// Important: for whatever reason, not using `document_start` causes the
|
|
||||||
// Promise returned by `tabs.executeScript()` to resolve only when the
|
|
||||||
// associated tab is closed.
|
|
||||||
case 'launchReporter': {
|
|
||||||
const pageStore = µb.pageStoreFromTabId(request.tabId);
|
|
||||||
if ( pageStore === null ) { break; }
|
|
||||||
if ( pageStore.hasUnprocessedRequest ) {
|
|
||||||
request.popupPanel.hasUnprocessedRequest = true;
|
|
||||||
}
|
|
||||||
vAPI.tabs.executeScript(request.tabId, {
|
|
||||||
allFrames: true,
|
|
||||||
file: '/js/scriptlets/cosmetic-report.js',
|
|
||||||
matchAboutBlank: true,
|
|
||||||
runAt: 'document_start',
|
|
||||||
}).then(results => {
|
|
||||||
const filters = results.reduce((a, v) => {
|
|
||||||
if ( Array.isArray(v) ) { a.push(...v); }
|
|
||||||
return a;
|
|
||||||
}, []);
|
|
||||||
if ( filters.length !== 0 ) {
|
|
||||||
request.popupPanel.cosmetic = filters;
|
|
||||||
}
|
|
||||||
const supportURL = new URL(vAPI.getURL('support.html'));
|
|
||||||
supportURL.searchParams.set('pageURL', request.pageURL);
|
|
||||||
supportURL.searchParams.set('popupPanel', JSON.stringify(request.popupPanel));
|
|
||||||
µb.openNewTab({ url: supportURL.href, select: true, index: -1 });
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -659,6 +676,15 @@ const onMessage = function(request, sender, callback) {
|
||||||
response = lastModified !== request.contentLastModified;
|
response = lastModified !== request.contentLastModified;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'launchReporter': {
|
||||||
|
launchReporter(request).then(url => {
|
||||||
|
if ( typeof url !== 'string' ) { return; }
|
||||||
|
µb.openNewTab({ url, select: true, index: -1 });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'revertFirewallRules':
|
case 'revertFirewallRules':
|
||||||
// TODO: use Set() to message around sets of hostnames
|
// TODO: use Set() to message around sets of hostnames
|
||||||
sessionFirewall.copyRules(
|
sessionFirewall.copyRules(
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
|
|
||||||
if ( typeof vAPI !== 'object' ) { return; }
|
if ( typeof vAPI !== 'object' ) { return; }
|
||||||
if ( typeof vAPI.domFilterer !== 'object' ) { return; }
|
if ( typeof vAPI.domFilterer !== 'object' ) { return; }
|
||||||
|
if ( vAPI.domFilterer === null ) { return; }
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -1470,18 +1470,12 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
|
||||||
// Respect cooldown period before launching an emergency update.
|
// Respect cooldown period before launching an emergency update.
|
||||||
const timeSinceLastEmergencyUpdate = (now - lastEmergencyUpdate) / 3600000;
|
const timeSinceLastEmergencyUpdate = (now - lastEmergencyUpdate) / 3600000;
|
||||||
if ( timeSinceLastEmergencyUpdate > 1 ) {
|
if ( timeSinceLastEmergencyUpdate > 1 ) {
|
||||||
const assetDict = await io.metadata();
|
const entries = await io.getUpdateAges({
|
||||||
for ( const [ assetKey, asset ] of Object.entries(assetDict) ) {
|
filters: µb.selectedFilterLists,
|
||||||
if ( asset.hasRemoteURL !== true ) { continue; }
|
internal: [ '*' ],
|
||||||
if ( asset.content === 'filters' ) {
|
});
|
||||||
if ( µb.selectedFilterLists.includes(assetKey) === false ) {
|
for ( const entry of entries ) {
|
||||||
continue;
|
if ( entry.ageNormalized < 2 ) { continue; }
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( asset.obsolete !== true ) { continue; }
|
|
||||||
const lastUpdateInDays = (now - asset.writeTime) / 86400000;
|
|
||||||
const daysSinceVeryObsolete = lastUpdateInDays - 2 * asset.updateAfter;
|
|
||||||
if ( daysSinceVeryObsolete < 0 ) { continue; }
|
|
||||||
needEmergencyUpdate = true;
|
needEmergencyUpdate = true;
|
||||||
lastEmergencyUpdate = now;
|
lastEmergencyUpdate = now;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -27,8 +27,6 @@ import { dom, qs$ } from './dom.js';
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
let supportData;
|
|
||||||
|
|
||||||
const uselessKeys = [
|
const uselessKeys = [
|
||||||
'modifiedHiddenSettings.benchmarkDatasetURL',
|
'modifiedHiddenSettings.benchmarkDatasetURL',
|
||||||
'modifiedHiddenSettings.blockingProfiles',
|
'modifiedHiddenSettings.blockingProfiles',
|
||||||
|
@ -138,7 +136,10 @@ function addDetailsToReportURL(id, collapse = false) {
|
||||||
dom.attr(elem, 'data-url', url);
|
dom.attr(elem, 'data-url', url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showData() {
|
async function showSupportData() {
|
||||||
|
const supportData = await vAPI.messaging.send('dashboard', {
|
||||||
|
what: 'getSupportData',
|
||||||
|
});
|
||||||
const shownData = JSON.parse(JSON.stringify(supportData));
|
const shownData = JSON.parse(JSON.stringify(supportData));
|
||||||
uselessKeys.forEach(prop => { removeKey(shownData, prop); });
|
uselessKeys.forEach(prop => { removeKey(shownData, prop); });
|
||||||
const redacted = true;
|
const redacted = true;
|
||||||
|
@ -196,6 +197,9 @@ const reportedPage = (( ) => {
|
||||||
dom.text(option, parsedURL.href);
|
dom.text(option, parsedURL.href);
|
||||||
select.append(option);
|
select.append(option);
|
||||||
}
|
}
|
||||||
|
if ( url.searchParams.get('shouldUpdate') !== null ) {
|
||||||
|
dom.cl.add(dom.body, 'shouldUpdate');
|
||||||
|
}
|
||||||
dom.cl.add(dom.body, 'filterIssue');
|
dom.cl.add(dom.body, 'filterIssue');
|
||||||
return {
|
return {
|
||||||
hostname: parsedURL.hostname.replace(/^(m|mobile|www)\./, ''),
|
hostname: parsedURL.hostname.replace(/^(m|mobile|www)\./, ''),
|
||||||
|
@ -210,7 +214,7 @@ function reportSpecificFilterType() {
|
||||||
return qs$('select[name="type"]').value;
|
return qs$('select[name="type"]').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportSpecificFilterIssue(ev) {
|
function reportSpecificFilterIssue() {
|
||||||
const githubURL = new URL('https://github.com/uBlockOrigin/uAssets/issues/new?template=specific_report_from_ubo.yml');
|
const githubURL = new URL('https://github.com/uBlockOrigin/uAssets/issues/new?template=specific_report_from_ubo.yml');
|
||||||
const issueType = reportSpecificFilterType();
|
const issueType = reportSpecificFilterType();
|
||||||
let title = `${reportedPage.hostname}: ${issueType}`;
|
let title = `${reportedPage.hostname}: ${issueType}`;
|
||||||
|
@ -228,9 +232,25 @@ function reportSpecificFilterIssue(ev) {
|
||||||
what: 'gotoURL',
|
what: 'gotoURL',
|
||||||
details: { url: githubURL.href, select: true, index: -1 },
|
details: { url: githubURL.href, select: true, index: -1 },
|
||||||
});
|
});
|
||||||
ev.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateFilterLists() {
|
||||||
|
dom.cl.add(dom.body, 'updating');
|
||||||
|
vAPI.messaging.send('dashboard', { what: 'forceUpdateAssets' });
|
||||||
|
}
|
||||||
|
|
||||||
|
vAPI.broadcastListener.add(msg => {
|
||||||
|
switch ( msg.what ) {
|
||||||
|
case 'assetsUpdated':
|
||||||
|
showSupportData();
|
||||||
|
dom.cl.remove(dom.body, 'updating');
|
||||||
|
dom.cl.add(dom.body, 'updated');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
const cmEditor = new CodeMirror(qs$('#supportData'), {
|
const cmEditor = new CodeMirror(qs$('#supportData'), {
|
||||||
|
@ -244,11 +264,7 @@ uBlockDashboard.patchCodeMirrorEditor(cmEditor);
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
(async ( ) => {
|
(async ( ) => {
|
||||||
supportData = await vAPI.messaging.send('dashboard', {
|
await showSupportData();
|
||||||
what: 'getSupportData',
|
|
||||||
});
|
|
||||||
|
|
||||||
showData();
|
|
||||||
|
|
||||||
dom.on('[data-url]', 'click', ev => {
|
dom.on('[data-url]', 'click', ev => {
|
||||||
const elem = ev.target.closest('[data-url]');
|
const elem = ev.target.closest('[data-url]');
|
||||||
|
@ -262,8 +278,16 @@ uBlockDashboard.patchCodeMirrorEditor(cmEditor);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ( reportedPage !== null ) {
|
if ( reportedPage !== null ) {
|
||||||
|
if ( dom.cl.has(dom.body, 'shouldUpdate') ) {
|
||||||
|
dom.on('.shouldUpdate button', 'click', ev => {
|
||||||
|
updateFilterLists();
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dom.on('[data-i18n="supportReportSpecificButton"]', 'click', ev => {
|
dom.on('[data-i18n="supportReportSpecificButton"]', 'click', ev => {
|
||||||
reportSpecificFilterIssue(ev);
|
reportSpecificFilterIssue();
|
||||||
|
ev.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
dom.on('[data-i18n="supportFindSpecificButton"]', 'click', ev => {
|
dom.on('[data-i18n="supportFindSpecificButton"]', 'click', ev => {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<link rel="stylesheet" href="css/themes/default.css">
|
<link rel="stylesheet" href="css/themes/default.css">
|
||||||
<link rel="stylesheet" href="css/common.css">
|
<link rel="stylesheet" href="css/common.css">
|
||||||
<link rel="stylesheet" href="css/dashboard-common.css">
|
<link rel="stylesheet" href="css/dashboard-common.css">
|
||||||
|
<link rel="stylesheet" href="css/fa-icons.css">
|
||||||
<link rel="stylesheet" href="css/support.css">
|
<link rel="stylesheet" href="css/support.css">
|
||||||
<link rel="stylesheet" href="css/codemirror.css">
|
<link rel="stylesheet" href="css/codemirror.css">
|
||||||
<link rel="shortcut icon" type="image/png" href="img/icon_64.png">
|
<link rel="shortcut icon" type="image/png" href="img/icon_64.png">
|
||||||
|
@ -62,34 +63,41 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="e">
|
<div class="e">
|
||||||
<h3 data-i18n="supportS6H"></h3>
|
<h3 data-i18n="supportS6H"></h3>
|
||||||
|
<p data-i18n="supportS3P1"></p>
|
||||||
|
<div class="supportEntry shouldUpdate">
|
||||||
|
<hr>
|
||||||
|
<p data-i18n="supportS6P2S1">_</p>
|
||||||
|
<button type="button" class="iconified"><span class="fa-icon">refresh</span><span data-i18n="3pUpdateNow">_</span><span class="hover"></span></button>
|
||||||
|
<u class="updated" data-i18n="supportS6P2S2">_</u>
|
||||||
|
</div>
|
||||||
<div class="supportEntry">
|
<div class="supportEntry">
|
||||||
<div>
|
<hr>
|
||||||
<p data-i18n="supportS3P1">
|
<p data-i18n="supportS6P1S1"></p>
|
||||||
<p data-i18n="supportS6P1S1">
|
|
||||||
</div>
|
|
||||||
<button type="button" data-i18n="supportFindSpecificButton">_<span class="hover"></span></button>
|
<button type="button" data-i18n="supportFindSpecificButton">_<span class="hover"></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="supportEntry">
|
<div class="supportEntry">
|
||||||
<div>
|
<hr>
|
||||||
<p>
|
<p>
|
||||||
<label data-i18n="supportS6URL"></label><br>
|
<label data-i18n="supportS6URL"></label><br>
|
||||||
<select name="url">
|
<select name="url">
|
||||||
<option></option>
|
<option></option>
|
||||||
</select>
|
</select>
|
||||||
<p>
|
</p>
|
||||||
<label data-i18n="supportS6Select1"></label><br>
|
<p>
|
||||||
<select name="type">
|
<label data-i18n="supportS6Select1"></label><br>
|
||||||
<option value="[unknown]" data-i18n="supportS6Select1Option0" selected disabled></option>
|
<select name="type">
|
||||||
<option value="ads" data-i18n="supportS6Select1Option1"></option>
|
<option value="[unknown]" data-i18n="supportS6Select1Option0" selected disabled></option>
|
||||||
<option value="detection" data-i18n="supportS6Select1Option3"></option>
|
<option value="ads" data-i18n="supportS6Select1Option1"></option>
|
||||||
<option value="popups" data-i18n="supportS6Select1Option6"></option>
|
<option value="detection" data-i18n="supportS6Select1Option3"></option>
|
||||||
<option value="nuisance" data-i18n="supportS6Select1Option2"></option>
|
<option value="popups" data-i18n="supportS6Select1Option6"></option>
|
||||||
<option value="breakage" data-i18n="supportS6Select1Option5"></option>
|
<option value="nuisance" data-i18n="supportS6Select1Option2"></option>
|
||||||
<option value="privacy" data-i18n="supportS6Select1Option4"></option>
|
<option value="breakage" data-i18n="supportS6Select1Option5"></option>
|
||||||
</select>
|
<option value="privacy" data-i18n="supportS6Select1Option4"></option>
|
||||||
<p>
|
</select>
|
||||||
<label><span class="input checkbox"><input id="isNSFW" type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span data-i18n="supportS6Checkbox1"></span></label>
|
</p>
|
||||||
</div>
|
<p>
|
||||||
|
<label><span class="input checkbox"><input id="isNSFW" type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span data-i18n="supportS6Checkbox1"></span></label>
|
||||||
|
</p>
|
||||||
<button type="button" data-i18n="supportReportSpecificButton" class="preferred">_<span class="hover"></span></button>
|
<button type="button" data-i18n="supportReportSpecificButton" class="preferred">_<span class="hover"></span></button>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -109,9 +117,11 @@
|
||||||
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
||||||
<script src="lib/hsluv/hsluv-0.1.0.min.js"></script>
|
<script src="lib/hsluv/hsluv-0.1.0.min.js"></script>
|
||||||
|
|
||||||
|
<script src="js/fa-icons.js" type="module"></script>
|
||||||
<script src="js/vapi.js"></script>
|
<script src="js/vapi.js"></script>
|
||||||
<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>
|
||||||
|
<script src="js/vapi-client-extra.js"></script>
|
||||||
<script src="js/theme.js" type="module"></script>
|
<script src="js/theme.js" type="module"></script>
|
||||||
<script src="js/i18n.js" type="module"></script>
|
<script src="js/i18n.js" type="module"></script>
|
||||||
<script src="js/dashboard-common.js" type="module"></script>
|
<script src="js/dashboard-common.js" type="module"></script>
|
||||||
|
|
Loading…
Reference in New Issue