code cosmetics: split manage.js
This commit is contained in:
parent
f001bca849
commit
f80f5612d6
|
@ -126,6 +126,9 @@
|
|||
<script src="js/prefs.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="manage/filters.js"></script>
|
||||
<script src="manage/updater-ui.js"></script>
|
||||
<script src="manage/object-diff.js"></script>
|
||||
<script src="manage/manage.js"></script>
|
||||
</head>
|
||||
|
||||
|
|
319
manage/filters.js
Normal file
319
manage/filters.js
Normal file
|
@ -0,0 +1,319 @@
|
|||
/* global installed */
|
||||
'use strict';
|
||||
|
||||
const filtersSelector = {
|
||||
hide: '',
|
||||
unhide: '',
|
||||
numShown: 0,
|
||||
numTotal: 0,
|
||||
};
|
||||
|
||||
|
||||
onDOMready().then(() => {
|
||||
$('#search').oninput = searchStyles;
|
||||
|
||||
$$('[data-filter]').forEach(el => {
|
||||
el.onchange = filterOnChange;
|
||||
if (el.closest('.hidden')) {
|
||||
el.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
// let manage::initGlobalEvents run onDOMready first
|
||||
debounce(filterOnChange, 0, {forceRefilter: true});
|
||||
});
|
||||
|
||||
|
||||
function filterOnChange({target: el, forceRefilter}) {
|
||||
const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim());
|
||||
if (!forceRefilter) {
|
||||
const value = getValue(el);
|
||||
if (value === el.lastValue) {
|
||||
return;
|
||||
}
|
||||
el.lastValue = value;
|
||||
}
|
||||
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el));
|
||||
const buildFilter = hide =>
|
||||
(hide ? '' : '.entry.hidden') +
|
||||
[...enabledFilters.map(el =>
|
||||
el.dataset[hide ? 'filterHide' : 'filter']
|
||||
.split(/,\s*/)
|
||||
.map(s => (hide ? '.entry:not(.hidden)' : '') + s)
|
||||
.join(','))
|
||||
].join(hide ? ',' : '');
|
||||
Object.assign(filtersSelector, {
|
||||
hide: buildFilter(true),
|
||||
unhide: buildFilter(false),
|
||||
});
|
||||
reapplyFilter();
|
||||
}
|
||||
|
||||
|
||||
function filterAndAppend({entry, container}) {
|
||||
if (!container) {
|
||||
container = [entry];
|
||||
// reverse the visibility, otherwise reapplyFilter will see no need to work
|
||||
if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
} else if ($('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container});
|
||||
}
|
||||
reapplyFilter(container);
|
||||
}
|
||||
|
||||
|
||||
function reapplyFilter(container = installed) {
|
||||
// A: show
|
||||
let toHide = [];
|
||||
let toUnhide = [];
|
||||
if (filtersSelector.hide) {
|
||||
filterContainer({hide: false});
|
||||
} else {
|
||||
toUnhide = container;
|
||||
}
|
||||
// showStyles() is building the page and no filters are active
|
||||
if (toUnhide instanceof DocumentFragment) {
|
||||
installed.appendChild(toUnhide);
|
||||
return;
|
||||
} else if (toUnhide.length && $('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container: toUnhide});
|
||||
filterContainer({hide: false});
|
||||
}
|
||||
// filtering needed or a single-element job from handleUpdate()
|
||||
const entries = installed.children;
|
||||
const numEntries = entries.length;
|
||||
let numVisible = numEntries - $$('.entry.hidden').length;
|
||||
for (const entry of toUnhide.children || toUnhide) {
|
||||
const next = findInsertionPoint(entry);
|
||||
if (entry.nextElementSibling !== next) {
|
||||
installed.insertBefore(entry, next);
|
||||
}
|
||||
if (entry.classList.contains('hidden')) {
|
||||
entry.classList.remove('hidden');
|
||||
numVisible++;
|
||||
}
|
||||
}
|
||||
// B: hide
|
||||
if (filtersSelector.hide) {
|
||||
filterContainer({hide: true});
|
||||
}
|
||||
if (!toHide.length) {
|
||||
showFiltersStats();
|
||||
return;
|
||||
}
|
||||
for (const entry of toHide) {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
// showStyles() is building the page with filters active so we need to:
|
||||
// 1. add all hidden entries to the end
|
||||
// 2. add the visible entries before the first hidden entry
|
||||
if (container instanceof DocumentFragment) {
|
||||
for (const entry of toHide) {
|
||||
installed.appendChild(entry);
|
||||
}
|
||||
installed.insertBefore(container, $('.entry.hidden'));
|
||||
showFiltersStats();
|
||||
return;
|
||||
}
|
||||
// normal filtering of the page or a single-element job from handleUpdate()
|
||||
// we need to keep the visible entries together at the start
|
||||
// first pass only moves one hidden entry in hidden groups with odd number of items
|
||||
shuffle(false);
|
||||
setTimeout(shuffle, 0, true);
|
||||
// single-element job from handleEvent(): add the last wraith
|
||||
if (toHide.length === 1 && toHide[0].parentElement !== installed) {
|
||||
installed.appendChild(toHide[0]);
|
||||
}
|
||||
showFiltersStats();
|
||||
return;
|
||||
|
||||
/***************************************/
|
||||
|
||||
function filterContainer({hide}) {
|
||||
const selector = filtersSelector[hide ? 'hide' : 'unhide'];
|
||||
if (container.filter) {
|
||||
if (hide) {
|
||||
// already filtered in previous invocation
|
||||
return;
|
||||
}
|
||||
for (const el of container) {
|
||||
(el.matches(selector) ? toUnhide : toHide).push(el);
|
||||
}
|
||||
return;
|
||||
} else if (hide) {
|
||||
toHide = $$(selector, container);
|
||||
} else {
|
||||
toUnhide = $$(selector, container);
|
||||
}
|
||||
}
|
||||
|
||||
function shuffle(fullPass) {
|
||||
if (fullPass && !document.body.classList.contains('update-in-progress')) {
|
||||
$('#check-all-updates').disabled = !$('.updatable:not(.can-update)');
|
||||
}
|
||||
// 1. skip the visible group on top
|
||||
let firstHidden = $('#installed > .hidden');
|
||||
let entry = firstHidden;
|
||||
let i = [...entries].indexOf(entry);
|
||||
let horizon = entries[numVisible];
|
||||
const skipGroup = state => {
|
||||
const start = i;
|
||||
const first = entry;
|
||||
while (entry && entry.classList.contains('hidden') === state) {
|
||||
entry = entry.nextElementSibling;
|
||||
i++;
|
||||
}
|
||||
return {first, start, len: i - start};
|
||||
};
|
||||
let prevGroup = i ? {first: entries[0], start: 0, len: i} : skipGroup(true);
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (entry) {
|
||||
// 2a. find the next hidden group's start and end
|
||||
// 2b. find the next visible group's start and end
|
||||
const isHidden = entry.classList.contains('hidden');
|
||||
const group = skipGroup(isHidden);
|
||||
const hidden = isHidden ? group : prevGroup;
|
||||
const visible = isHidden ? prevGroup : group;
|
||||
// 3. move the shortest group; repeat 2-3
|
||||
if (hidden.len < visible.len && (fullPass || hidden.len % 2)) {
|
||||
// 3a. move hidden under the horizon
|
||||
for (let j = 0; j < (fullPass ? hidden.len : 1); j++) {
|
||||
const entry = entries[hidden.start];
|
||||
installed.insertBefore(entry, horizon);
|
||||
horizon = entry;
|
||||
i--;
|
||||
}
|
||||
prevGroup = isHidden ? skipGroup(false) : group;
|
||||
firstHidden = entry;
|
||||
} else if (isHidden || !fullPass) {
|
||||
prevGroup = group;
|
||||
} else {
|
||||
// 3b. move visible above the horizon
|
||||
for (let j = 0; j < visible.len; j++) {
|
||||
const entry = entries[visible.start + j];
|
||||
installed.insertBefore(entry, firstHidden);
|
||||
}
|
||||
prevGroup = {
|
||||
first: firstHidden,
|
||||
start: hidden.start + visible.len,
|
||||
len: hidden.len + skipGroup(true).len,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findInsertionPoint(entry) {
|
||||
const nameLLC = entry.styleNameLowerCase;
|
||||
let a = 0;
|
||||
let b = Math.min(numEntries, numVisible) - 1;
|
||||
if (b < 0) {
|
||||
return entries[numVisible];
|
||||
}
|
||||
if (entries[0].styleNameLowerCase > nameLLC) {
|
||||
return entries[0];
|
||||
}
|
||||
if (entries[b].styleNameLowerCase <= nameLLC) {
|
||||
return entries[numVisible];
|
||||
}
|
||||
// bisect
|
||||
while (a < b - 1) {
|
||||
const c = (a + b) / 2 | 0;
|
||||
if (nameLLC < entries[c].styleNameLowerCase) {
|
||||
b = c;
|
||||
} else {
|
||||
a = c;
|
||||
}
|
||||
}
|
||||
if (entries[a].styleNameLowerCase > nameLLC) {
|
||||
return entries[a];
|
||||
}
|
||||
while (a <= b && entries[a].styleNameLowerCase < nameLLC) {
|
||||
a++;
|
||||
}
|
||||
return entries[entries[a].styleNameLowerCase <= nameLLC ? a + 1 : a];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showFiltersStats({immediately} = {}) {
|
||||
if (!immediately) {
|
||||
debounce(showFiltersStats, 100, {immediately: true});
|
||||
return;
|
||||
}
|
||||
$('#filters').classList.toggle('active', filtersSelector.hide !== '');
|
||||
const numTotal = BG.cachedStyles.list.length;
|
||||
const numHidden = installed.getElementsByClassName('entry hidden').length;
|
||||
const numShown = Math.min(numTotal - numHidden, installed.children.length);
|
||||
if (filtersSelector.numShown !== numShown ||
|
||||
filtersSelector.numTotal !== numTotal) {
|
||||
filtersSelector.numShown = numShown;
|
||||
filtersSelector.numTotal = numTotal;
|
||||
$('#filters-stats').textContent = t('filteredStyles', [numShown, numTotal]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function searchStyles({immediately, container}) {
|
||||
const searchElement = $('#search');
|
||||
const query = searchElement.value.toLocaleLowerCase();
|
||||
const queryPrev = searchElement.lastValue || '';
|
||||
if (query === queryPrev && !immediately && !container) {
|
||||
return;
|
||||
}
|
||||
if (!immediately) {
|
||||
debounce(searchStyles, 150, {immediately: true});
|
||||
return;
|
||||
}
|
||||
searchElement.lastValue = query;
|
||||
|
||||
const searchInVisible = queryPrev && query.includes(queryPrev);
|
||||
const entries = container && container.children || container ||
|
||||
(searchInVisible ? $$('.entry:not(.hidden)') : installed.children);
|
||||
let needsRefilter = false;
|
||||
for (const entry of entries) {
|
||||
let isMatching = !query;
|
||||
if (!isMatching) {
|
||||
const style = BG.cachedStyles.byId.get(entry.styleId) || {};
|
||||
isMatching = Boolean(style && (
|
||||
isMatchingText(style.name) ||
|
||||
style.url && isMatchingText(style.url) ||
|
||||
isMatchingStyle(style)));
|
||||
}
|
||||
if (entry.classList.contains('not-matching') !== !isMatching) {
|
||||
entry.classList.toggle('not-matching', !isMatching);
|
||||
needsRefilter = true;
|
||||
}
|
||||
}
|
||||
if (needsRefilter && !container) {
|
||||
filterOnChange({forceRefilter: true});
|
||||
}
|
||||
return;
|
||||
|
||||
function isMatchingStyle(style) {
|
||||
for (const section of style.sections) {
|
||||
for (const prop in section) {
|
||||
const value = section[prop];
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isMatchingText(value)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
for (const str of value) {
|
||||
if (isMatchingText(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMatchingText(text) {
|
||||
return text.toLocaleLowerCase().indexOf(query) >= 0;
|
||||
}
|
||||
}
|
534
manage/manage.js
534
manage/manage.js
|
@ -1,13 +1,10 @@
|
|||
/* global messageBox, getStyleWithNoCode, retranslateCSS */
|
||||
/* global filtersSelector, filterAndAppend */
|
||||
/* global checkUpdate, handleUpdateInstalled */
|
||||
/* global objectDiff */
|
||||
'use strict';
|
||||
|
||||
let installed;
|
||||
const filtersSelector = {
|
||||
hide: '',
|
||||
unhide: '',
|
||||
numShown: 0,
|
||||
numTotal: 0,
|
||||
};
|
||||
|
||||
const ENTRY_ID_PREFIX_RAW = 'style-';
|
||||
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
|
||||
|
@ -64,11 +61,6 @@ function onRuntimeMessage(msg) {
|
|||
function initGlobalEvents() {
|
||||
installed = $('#installed');
|
||||
installed.onclick = handleEvent.entryClicked;
|
||||
$('#check-all-updates').onclick = checkUpdateAll;
|
||||
$('#check-all-updates-force').onclick = checkUpdateAll;
|
||||
$('#apply-all-updates').onclick = applyUpdateAll;
|
||||
$('#update-history').onclick = showUpdateHistory;
|
||||
$('#search').oninput = searchStyles;
|
||||
$('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage();
|
||||
$('#manage-shortcuts-button').onclick = () => openURL({url: URLS.configureCommands});
|
||||
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
|
||||
|
@ -98,14 +90,6 @@ function initGlobalEvents() {
|
|||
// N.B. triggers existing onchange listeners
|
||||
setupLivePrefs();
|
||||
|
||||
$$('[data-filter]').forEach(el => {
|
||||
el.onchange = handleEvent.filterOnChange;
|
||||
if (el.closest('.hidden')) {
|
||||
el.checked = false;
|
||||
}
|
||||
});
|
||||
handleEvent.filterOnChange({forceRefilter: true});
|
||||
|
||||
$$('[id^="manage.newUI"]')
|
||||
.forEach(el => (el.oninput = (el.onchange = switchUI)));
|
||||
|
||||
|
@ -386,31 +370,6 @@ Object.assign(handleEvent, {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
filterOnChange({target: el, forceRefilter}) {
|
||||
const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim());
|
||||
if (!forceRefilter) {
|
||||
const value = getValue(el);
|
||||
if (value === el.lastValue) {
|
||||
return;
|
||||
}
|
||||
el.lastValue = value;
|
||||
}
|
||||
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el));
|
||||
const buildFilter = hide =>
|
||||
(hide ? '' : '.entry.hidden') +
|
||||
[...enabledFilters.map(el =>
|
||||
el.dataset[hide ? 'filterHide' : 'filter']
|
||||
.split(/,\s*/)
|
||||
.map(s => (hide ? '.entry:not(.hidden)' : '') + s)
|
||||
.join(','))
|
||||
].join(hide ? ',' : '');
|
||||
Object.assign(filtersSelector, {
|
||||
hide: buildFilter(true),
|
||||
unhide: buildFilter(false),
|
||||
});
|
||||
reapplyFilter();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
@ -429,7 +388,7 @@ function handleUpdate(style, {reason, method} = {}) {
|
|||
}
|
||||
}
|
||||
if (reason === 'update' && entry.matches('.updatable')) {
|
||||
handleUpdateInstalled();
|
||||
handleUpdateInstalled(entry);
|
||||
}
|
||||
filterAndAppend({entry});
|
||||
if (!entry.matches('.hidden') && reason !== 'import') {
|
||||
|
@ -454,13 +413,6 @@ function handleUpdate(style, {reason, method} = {}) {
|
|||
oldEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleUpdateInstalled() {
|
||||
entry.classList.add('update-done');
|
||||
entry.classList.remove('can-update', 'updatable');
|
||||
$('.update-note', entry).textContent = t('updateCompleted');
|
||||
renderUpdatesOnlyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -547,484 +499,6 @@ function switchUI({styleOnly} = {}) {
|
|||
}
|
||||
|
||||
|
||||
function applyUpdateAll() {
|
||||
const btnApply = $('#apply-all-updates');
|
||||
btnApply.disabled = true;
|
||||
setTimeout(() => {
|
||||
btnApply.classList.add('hidden');
|
||||
btnApply.disabled = false;
|
||||
renderUpdatesOnlyFilter({show: false});
|
||||
}, 1000);
|
||||
|
||||
$$('.can-update .update').forEach(button => {
|
||||
scrollElementIntoView(button);
|
||||
button.click();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function checkUpdateAll() {
|
||||
document.body.classList.add('update-in-progress');
|
||||
$('#check-all-updates').disabled = true;
|
||||
$('#check-all-updates-force').classList.add('hidden');
|
||||
$('#apply-all-updates').classList.add('hidden');
|
||||
$('#update-all-no-updates').classList.add('hidden');
|
||||
|
||||
const ignoreDigest = this && this.id === 'check-all-updates-force';
|
||||
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
|
||||
.map(el => checkUpdate(el, {single: false}));
|
||||
|
||||
let total = 0;
|
||||
let checked = 0;
|
||||
let skippedEdited = 0;
|
||||
let updated = 0;
|
||||
|
||||
BG.updater.checkAllStyles({observer, save: false, ignoreDigest}).then(done);
|
||||
|
||||
function observer(state, value, details) {
|
||||
switch (state) {
|
||||
case BG.updater.COUNT:
|
||||
total = value;
|
||||
break;
|
||||
case BG.updater.UPDATED:
|
||||
if (++updated === 1) {
|
||||
$('#apply-all-updates').disabled = true;
|
||||
$('#apply-all-updates').classList.remove('hidden');
|
||||
}
|
||||
$('#apply-all-updates').dataset.value = updated;
|
||||
// fallthrough
|
||||
case BG.updater.SKIPPED:
|
||||
checked++;
|
||||
if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) {
|
||||
skippedEdited++;
|
||||
}
|
||||
reportUpdateState(state, value, details);
|
||||
break;
|
||||
}
|
||||
const progress = $('#update-progress');
|
||||
const maxWidth = progress.parentElement.clientWidth;
|
||||
progress.style.width = Math.round(checked / total * maxWidth) + 'px';
|
||||
}
|
||||
|
||||
function done() {
|
||||
document.body.classList.remove('update-in-progress');
|
||||
$('#check-all-updates').disabled = total === 0;
|
||||
$('#apply-all-updates').disabled = false;
|
||||
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
|
||||
if (!updated) {
|
||||
$('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0;
|
||||
$('#update-all-no-updates').classList.remove('hidden');
|
||||
$('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkUpdate(entry, {single = true} = {}) {
|
||||
$('.update-note', entry).textContent = t('checkingForUpdate');
|
||||
$('.check-update', entry).title = '';
|
||||
if (single) {
|
||||
BG.updater.checkStyle({
|
||||
save: false,
|
||||
ignoreDigest: entry.classList.contains('update-problem'),
|
||||
style: BG.cachedStyles.byId.get(entry.styleId),
|
||||
observer: reportUpdateState,
|
||||
});
|
||||
}
|
||||
entry.classList.remove('checking-update', 'no-update', 'update-problem');
|
||||
entry.classList.add('checking-update');
|
||||
}
|
||||
|
||||
|
||||
function reportUpdateState(state, style, details) {
|
||||
const entry = $(ENTRY_ID_PREFIX + style.id);
|
||||
entry.classList.remove('checking-update');
|
||||
switch (state) {
|
||||
case BG.updater.UPDATED:
|
||||
entry.classList.add('can-update');
|
||||
entry.updatedCode = style;
|
||||
$('.update-note', entry).textContent = '';
|
||||
$('#onlyUpdates').classList.remove('hidden');
|
||||
break;
|
||||
case BG.updater.SKIPPED: {
|
||||
if (entry.classList.contains('can-update')) {
|
||||
break;
|
||||
}
|
||||
const same = details === BG.updater.SAME_MD5 || details === BG.updater.SAME_CODE;
|
||||
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
|
||||
entry.dataset.details = details;
|
||||
if (!details) {
|
||||
details = t('updateCheckFailServerUnreachable');
|
||||
} else if (typeof details === 'number') {
|
||||
details = t('updateCheckFailBadResponseCode', [details]);
|
||||
} else if (details === BG.updater.EDITED) {
|
||||
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
} else if (details === BG.updater.MAYBE_EDITED) {
|
||||
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
}
|
||||
const message = same ? t('updateCheckSucceededNoUpdate') : details;
|
||||
entry.classList.add('no-update');
|
||||
entry.classList.toggle('update-problem', !same);
|
||||
$('.update-note', entry).textContent = message;
|
||||
$('.check-update', entry).title = newUI.enabled ? message : '';
|
||||
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
||||
if (!document.body.classList.contains('update-in-progress')) {
|
||||
// this is a single update job so we can decide whether to hide the filter
|
||||
renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filtersSelector.hide) {
|
||||
filterAndAppend({entry});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderUpdatesOnlyFilter({show, check} = {}) {
|
||||
const numUpdatable = $$('.can-update').length;
|
||||
const mightUpdate = numUpdatable > 0 || $('.update-problem');
|
||||
const checkbox = $('#onlyUpdates input');
|
||||
show = show !== undefined ? show : mightUpdate;
|
||||
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
|
||||
|
||||
$('#onlyUpdates').classList.toggle('hidden', !show);
|
||||
checkbox.checked = check;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
|
||||
const btnApply = $('#apply-all-updates');
|
||||
if (!btnApply.matches('.hidden')) {
|
||||
if (numUpdatable > 0) {
|
||||
btnApply.dataset.value = numUpdatable;
|
||||
} else {
|
||||
btnApply.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showUpdateHistory() {
|
||||
BG.chromeLocal.getValue('updateLog').then((lines = []) => {
|
||||
messageBox({
|
||||
title: t('updateCheckHistory'),
|
||||
contents: $element({
|
||||
className: 'update-history-log',
|
||||
textContent: lines.join('\n'),
|
||||
}),
|
||||
buttons: [t('confirmOK')],
|
||||
onshow: () => ($('#message-box-contents').scrollTop = 1e9),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function searchStyles({immediately, container}) {
|
||||
const searchElement = $('#search');
|
||||
const query = searchElement.value.toLocaleLowerCase();
|
||||
const queryPrev = searchElement.lastValue || '';
|
||||
if (query === queryPrev && !immediately && !container) {
|
||||
return;
|
||||
}
|
||||
if (!immediately) {
|
||||
debounce(searchStyles, 150, {immediately: true});
|
||||
return;
|
||||
}
|
||||
searchElement.lastValue = query;
|
||||
|
||||
const searchInVisible = queryPrev && query.includes(queryPrev);
|
||||
const entries = container && container.children || container ||
|
||||
(searchInVisible ? $$('.entry:not(.hidden)') : installed.children);
|
||||
let needsRefilter = false;
|
||||
for (const entry of entries) {
|
||||
let isMatching = !query;
|
||||
if (!isMatching) {
|
||||
const style = BG.cachedStyles.byId.get(entry.styleId) || {};
|
||||
isMatching = Boolean(style && (
|
||||
isMatchingText(style.name) ||
|
||||
style.url && isMatchingText(style.url) ||
|
||||
isMatchingStyle(style)));
|
||||
}
|
||||
if (entry.classList.contains('not-matching') !== !isMatching) {
|
||||
entry.classList.toggle('not-matching', !isMatching);
|
||||
needsRefilter = true;
|
||||
}
|
||||
}
|
||||
if (needsRefilter && !container) {
|
||||
handleEvent.filterOnChange({forceRefilter: true});
|
||||
}
|
||||
return;
|
||||
|
||||
function isMatchingStyle(style) {
|
||||
for (const section of style.sections) {
|
||||
for (const prop in section) {
|
||||
const value = section[prop];
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isMatchingText(value)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
for (const str of value) {
|
||||
if (isMatchingText(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMatchingText(text) {
|
||||
return text.toLocaleLowerCase().indexOf(query) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function filterAndAppend({entry, container}) {
|
||||
if (!container) {
|
||||
container = [entry];
|
||||
// reverse the visibility, otherwise reapplyFilter will see no need to work
|
||||
if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
} else if ($('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container});
|
||||
}
|
||||
reapplyFilter(container);
|
||||
}
|
||||
|
||||
|
||||
function reapplyFilter(container = installed) {
|
||||
// A: show
|
||||
let toHide = [];
|
||||
let toUnhide = [];
|
||||
if (filtersSelector.hide) {
|
||||
filterContainer({hide: false});
|
||||
} else {
|
||||
toUnhide = container;
|
||||
}
|
||||
// showStyles() is building the page and no filters are active
|
||||
if (toUnhide instanceof DocumentFragment) {
|
||||
installed.appendChild(toUnhide);
|
||||
return;
|
||||
} else if (toUnhide.length && $('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container: toUnhide});
|
||||
filterContainer({hide: false});
|
||||
}
|
||||
// filtering needed or a single-element job from handleUpdate()
|
||||
const entries = installed.children;
|
||||
const numEntries = entries.length;
|
||||
let numVisible = numEntries - $$('.entry.hidden').length;
|
||||
for (const entry of toUnhide.children || toUnhide) {
|
||||
const next = findInsertionPoint(entry);
|
||||
if (entry.nextElementSibling !== next) {
|
||||
installed.insertBefore(entry, next);
|
||||
}
|
||||
if (entry.classList.contains('hidden')) {
|
||||
entry.classList.remove('hidden');
|
||||
numVisible++;
|
||||
}
|
||||
}
|
||||
// B: hide
|
||||
if (filtersSelector.hide) {
|
||||
filterContainer({hide: true});
|
||||
}
|
||||
if (!toHide.length) {
|
||||
showFiltersStats();
|
||||
return;
|
||||
}
|
||||
for (const entry of toHide) {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
// showStyles() is building the page with filters active so we need to:
|
||||
// 1. add all hidden entries to the end
|
||||
// 2. add the visible entries before the first hidden entry
|
||||
if (container instanceof DocumentFragment) {
|
||||
for (const entry of toHide) {
|
||||
installed.appendChild(entry);
|
||||
}
|
||||
installed.insertBefore(container, $('.entry.hidden'));
|
||||
showFiltersStats();
|
||||
return;
|
||||
}
|
||||
// normal filtering of the page or a single-element job from handleUpdate()
|
||||
// we need to keep the visible entries together at the start
|
||||
// first pass only moves one hidden entry in hidden groups with odd number of items
|
||||
shuffle(false);
|
||||
setTimeout(shuffle, 0, true);
|
||||
// single-element job from handleEvent(): add the last wraith
|
||||
if (toHide.length === 1 && toHide[0].parentElement !== installed) {
|
||||
installed.appendChild(toHide[0]);
|
||||
}
|
||||
showFiltersStats();
|
||||
return;
|
||||
|
||||
/***************************************/
|
||||
|
||||
function filterContainer({hide}) {
|
||||
const selector = filtersSelector[hide ? 'hide' : 'unhide'];
|
||||
if (container.filter) {
|
||||
if (hide) {
|
||||
// already filtered in previous invocation
|
||||
return;
|
||||
}
|
||||
for (const el of container) {
|
||||
(el.matches(selector) ? toUnhide : toHide).push(el);
|
||||
}
|
||||
return;
|
||||
} else if (hide) {
|
||||
toHide = $$(selector, container);
|
||||
} else {
|
||||
toUnhide = $$(selector, container);
|
||||
}
|
||||
}
|
||||
|
||||
function shuffle(fullPass) {
|
||||
if (fullPass && !document.body.classList.contains('update-in-progress')) {
|
||||
$('#check-all-updates').disabled = !$('.updatable:not(.can-update)');
|
||||
}
|
||||
// 1. skip the visible group on top
|
||||
let firstHidden = $('#installed > .hidden');
|
||||
let entry = firstHidden;
|
||||
let i = [...entries].indexOf(entry);
|
||||
let horizon = entries[numVisible];
|
||||
const skipGroup = state => {
|
||||
const start = i;
|
||||
const first = entry;
|
||||
while (entry && entry.classList.contains('hidden') === state) {
|
||||
entry = entry.nextElementSibling;
|
||||
i++;
|
||||
}
|
||||
return {first, start, len: i - start};
|
||||
};
|
||||
let prevGroup = i ? {first: entries[0], start: 0, len: i} : skipGroup(true);
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (entry) {
|
||||
// 2a. find the next hidden group's start and end
|
||||
// 2b. find the next visible group's start and end
|
||||
const isHidden = entry.classList.contains('hidden');
|
||||
const group = skipGroup(isHidden);
|
||||
const hidden = isHidden ? group : prevGroup;
|
||||
const visible = isHidden ? prevGroup : group;
|
||||
// 3. move the shortest group; repeat 2-3
|
||||
if (hidden.len < visible.len && (fullPass || hidden.len % 2)) {
|
||||
// 3a. move hidden under the horizon
|
||||
for (let j = 0; j < (fullPass ? hidden.len : 1); j++) {
|
||||
const entry = entries[hidden.start];
|
||||
installed.insertBefore(entry, horizon);
|
||||
horizon = entry;
|
||||
i--;
|
||||
}
|
||||
prevGroup = isHidden ? skipGroup(false) : group;
|
||||
firstHidden = entry;
|
||||
} else if (isHidden || !fullPass) {
|
||||
prevGroup = group;
|
||||
} else {
|
||||
// 3b. move visible above the horizon
|
||||
for (let j = 0; j < visible.len; j++) {
|
||||
const entry = entries[visible.start + j];
|
||||
installed.insertBefore(entry, firstHidden);
|
||||
}
|
||||
prevGroup = {
|
||||
first: firstHidden,
|
||||
start: hidden.start + visible.len,
|
||||
len: hidden.len + skipGroup(true).len,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findInsertionPoint(entry) {
|
||||
const nameLLC = entry.styleNameLowerCase;
|
||||
let a = 0;
|
||||
let b = Math.min(numEntries, numVisible) - 1;
|
||||
if (b < 0) {
|
||||
return entries[numVisible];
|
||||
}
|
||||
if (entries[0].styleNameLowerCase > nameLLC) {
|
||||
return entries[0];
|
||||
}
|
||||
if (entries[b].styleNameLowerCase <= nameLLC) {
|
||||
return entries[numVisible];
|
||||
}
|
||||
// bisect
|
||||
while (a < b - 1) {
|
||||
const c = (a + b) / 2 | 0;
|
||||
if (nameLLC < entries[c].styleNameLowerCase) {
|
||||
b = c;
|
||||
} else {
|
||||
a = c;
|
||||
}
|
||||
}
|
||||
if (entries[a].styleNameLowerCase > nameLLC) {
|
||||
return entries[a];
|
||||
}
|
||||
while (a <= b && entries[a].styleNameLowerCase < nameLLC) {
|
||||
a++;
|
||||
}
|
||||
return entries[entries[a].styleNameLowerCase <= nameLLC ? a + 1 : a];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showFiltersStats({immediately} = {}) {
|
||||
if (!immediately) {
|
||||
debounce(showFiltersStats, 100, {immediately: true});
|
||||
return;
|
||||
}
|
||||
$('#filters').classList.toggle('active', filtersSelector.hide !== '');
|
||||
const numTotal = BG.cachedStyles.list.length;
|
||||
const numHidden = installed.getElementsByClassName('entry hidden').length;
|
||||
const numShown = Math.min(numTotal - numHidden, installed.children.length);
|
||||
if (filtersSelector.numShown !== numShown ||
|
||||
filtersSelector.numTotal !== numTotal) {
|
||||
filtersSelector.numShown = numShown;
|
||||
filtersSelector.numTotal = numTotal;
|
||||
$('#filters-stats').textContent = t('filteredStyles', [numShown, numTotal]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function rememberScrollPosition() {
|
||||
history.replaceState({scrollY: window.scrollY}, document.title);
|
||||
}
|
||||
|
||||
|
||||
function objectDiff(first, second, path = '') {
|
||||
const diff = [];
|
||||
for (const key in first) {
|
||||
const a = first[key];
|
||||
const b = second[key];
|
||||
if (a === b) {
|
||||
continue;
|
||||
}
|
||||
if (b === undefined) {
|
||||
diff.push({path, key, values: [a], type: 'removed'});
|
||||
continue;
|
||||
}
|
||||
if (a && typeof a.filter === 'function' && b && typeof b.filter === 'function') {
|
||||
if (
|
||||
a.length !== b.length ||
|
||||
a.some((el, i) => {
|
||||
const result = !el || typeof el !== 'object'
|
||||
? el !== b[i]
|
||||
: objectDiff(el, b[i], path + key + '[' + i + '].').length;
|
||||
return result;
|
||||
})
|
||||
) {
|
||||
diff.push({path, key, values: [a, b], type: 'changed'});
|
||||
}
|
||||
} else if (typeof a === 'object' && typeof b === 'object') {
|
||||
diff.push(...objectDiff(a, b, path + key + '.'));
|
||||
} else {
|
||||
diff.push({path, key, values: [a, b], type: 'changed'});
|
||||
}
|
||||
}
|
||||
for (const key in second) {
|
||||
if (!(key in first)) {
|
||||
diff.push({path, key, values: [second[key]], type: 'added'});
|
||||
}
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
|
39
manage/object-diff.js
Normal file
39
manage/object-diff.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
function objectDiff(first, second, path = '') {
|
||||
const diff = [];
|
||||
for (const key in first) {
|
||||
const a = first[key];
|
||||
const b = second[key];
|
||||
if (a === b) {
|
||||
continue;
|
||||
}
|
||||
if (b === undefined) {
|
||||
diff.push({path, key, values: [a], type: 'removed'});
|
||||
continue;
|
||||
}
|
||||
if (a && typeof a.filter === 'function' && b && typeof b.filter === 'function') {
|
||||
if (
|
||||
a.length !== b.length ||
|
||||
a.some((el, i) => {
|
||||
const result = !el || typeof el !== 'object'
|
||||
? el !== b[i]
|
||||
: objectDiff(el, b[i], path + key + '[' + i + '].').length;
|
||||
return result;
|
||||
})
|
||||
) {
|
||||
diff.push({path, key, values: [a, b], type: 'changed'});
|
||||
}
|
||||
} else if (typeof a === 'object' && typeof b === 'object') {
|
||||
diff.push(...objectDiff(a, b, path + key + '.'));
|
||||
} else {
|
||||
diff.push({path, key, values: [a, b], type: 'changed'});
|
||||
}
|
||||
}
|
||||
for (const key in second) {
|
||||
if (!(key in first)) {
|
||||
diff.push({path, key, values: [second[key]], type: 'added'});
|
||||
}
|
||||
}
|
||||
return diff;
|
||||
}
|
189
manage/updater-ui.js
Normal file
189
manage/updater-ui.js
Normal file
|
@ -0,0 +1,189 @@
|
|||
/* global messageBox */
|
||||
/* global ENTRY_ID_PREFIX, newUI */
|
||||
/* global filtersSelector, filterAndAppend */
|
||||
'use strict';
|
||||
|
||||
onDOMready().then(() => {
|
||||
$('#check-all-updates').onclick = checkUpdateAll;
|
||||
$('#check-all-updates-force').onclick = checkUpdateAll;
|
||||
$('#apply-all-updates').onclick = applyUpdateAll;
|
||||
$('#update-history').onclick = showUpdateHistory;
|
||||
});
|
||||
|
||||
|
||||
function applyUpdateAll() {
|
||||
const btnApply = $('#apply-all-updates');
|
||||
btnApply.disabled = true;
|
||||
setTimeout(() => {
|
||||
btnApply.classList.add('hidden');
|
||||
btnApply.disabled = false;
|
||||
renderUpdatesOnlyFilter({show: false});
|
||||
}, 1000);
|
||||
|
||||
$$('.can-update .update').forEach(button => {
|
||||
scrollElementIntoView(button);
|
||||
button.click();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function checkUpdateAll() {
|
||||
document.body.classList.add('update-in-progress');
|
||||
$('#check-all-updates').disabled = true;
|
||||
$('#check-all-updates-force').classList.add('hidden');
|
||||
$('#apply-all-updates').classList.add('hidden');
|
||||
$('#update-all-no-updates').classList.add('hidden');
|
||||
|
||||
const ignoreDigest = this && this.id === 'check-all-updates-force';
|
||||
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
|
||||
.map(el => checkUpdate(el, {single: false}));
|
||||
|
||||
let total = 0;
|
||||
let checked = 0;
|
||||
let skippedEdited = 0;
|
||||
let updated = 0;
|
||||
|
||||
BG.updater.checkAllStyles({observer, save: false, ignoreDigest}).then(done);
|
||||
|
||||
function observer(state, value, details) {
|
||||
switch (state) {
|
||||
case BG.updater.COUNT:
|
||||
total = value;
|
||||
break;
|
||||
case BG.updater.UPDATED:
|
||||
if (++updated === 1) {
|
||||
$('#apply-all-updates').disabled = true;
|
||||
$('#apply-all-updates').classList.remove('hidden');
|
||||
}
|
||||
$('#apply-all-updates').dataset.value = updated;
|
||||
// fallthrough
|
||||
case BG.updater.SKIPPED:
|
||||
checked++;
|
||||
if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) {
|
||||
skippedEdited++;
|
||||
}
|
||||
reportUpdateState(state, value, details);
|
||||
break;
|
||||
}
|
||||
const progress = $('#update-progress');
|
||||
const maxWidth = progress.parentElement.clientWidth;
|
||||
progress.style.width = Math.round(checked / total * maxWidth) + 'px';
|
||||
}
|
||||
|
||||
function done() {
|
||||
document.body.classList.remove('update-in-progress');
|
||||
$('#check-all-updates').disabled = total === 0;
|
||||
$('#apply-all-updates').disabled = false;
|
||||
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
|
||||
if (!updated) {
|
||||
$('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0;
|
||||
$('#update-all-no-updates').classList.remove('hidden');
|
||||
$('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkUpdate(entry, {single = true} = {}) {
|
||||
$('.update-note', entry).textContent = t('checkingForUpdate');
|
||||
$('.check-update', entry).title = '';
|
||||
if (single) {
|
||||
BG.updater.checkStyle({
|
||||
save: false,
|
||||
ignoreDigest: entry.classList.contains('update-problem'),
|
||||
style: BG.cachedStyles.byId.get(entry.styleId),
|
||||
observer: reportUpdateState,
|
||||
});
|
||||
}
|
||||
entry.classList.remove('checking-update', 'no-update', 'update-problem');
|
||||
entry.classList.add('checking-update');
|
||||
}
|
||||
|
||||
|
||||
function reportUpdateState(state, style, details) {
|
||||
const entry = $(ENTRY_ID_PREFIX + style.id);
|
||||
entry.classList.remove('checking-update');
|
||||
switch (state) {
|
||||
case BG.updater.UPDATED:
|
||||
entry.classList.add('can-update');
|
||||
entry.updatedCode = style;
|
||||
$('.update-note', entry).textContent = '';
|
||||
$('#onlyUpdates').classList.remove('hidden');
|
||||
break;
|
||||
case BG.updater.SKIPPED: {
|
||||
if (entry.classList.contains('can-update')) {
|
||||
break;
|
||||
}
|
||||
const same = details === BG.updater.SAME_MD5 || details === BG.updater.SAME_CODE;
|
||||
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
|
||||
entry.dataset.details = details;
|
||||
if (!details) {
|
||||
details = t('updateCheckFailServerUnreachable');
|
||||
} else if (typeof details === 'number') {
|
||||
details = t('updateCheckFailBadResponseCode', [details]);
|
||||
} else if (details === BG.updater.EDITED) {
|
||||
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
} else if (details === BG.updater.MAYBE_EDITED) {
|
||||
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
}
|
||||
const message = same ? t('updateCheckSucceededNoUpdate') : details;
|
||||
entry.classList.add('no-update');
|
||||
entry.classList.toggle('update-problem', !same);
|
||||
$('.update-note', entry).textContent = message;
|
||||
$('.check-update', entry).title = newUI.enabled ? message : '';
|
||||
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
||||
if (!document.body.classList.contains('update-in-progress')) {
|
||||
// this is a single update job so we can decide whether to hide the filter
|
||||
renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filtersSelector.hide) {
|
||||
filterAndAppend({entry});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderUpdatesOnlyFilter({show, check} = {}) {
|
||||
const numUpdatable = $$('.can-update').length;
|
||||
const mightUpdate = numUpdatable > 0 || $('.update-problem');
|
||||
const checkbox = $('#onlyUpdates input');
|
||||
show = show !== undefined ? show : mightUpdate;
|
||||
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
|
||||
|
||||
$('#onlyUpdates').classList.toggle('hidden', !show);
|
||||
checkbox.checked = check;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
|
||||
const btnApply = $('#apply-all-updates');
|
||||
if (!btnApply.matches('.hidden')) {
|
||||
if (numUpdatable > 0) {
|
||||
btnApply.dataset.value = numUpdatable;
|
||||
} else {
|
||||
btnApply.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showUpdateHistory() {
|
||||
BG.chromeLocal.getValue('updateLog').then((lines = []) => {
|
||||
messageBox({
|
||||
title: t('updateCheckHistory'),
|
||||
contents: $element({
|
||||
className: 'update-history-log',
|
||||
textContent: lines.join('\n'),
|
||||
}),
|
||||
buttons: [t('confirmOK')],
|
||||
onshow: () => ($('#message-box-contents').scrollTop = 1e9),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function handleUpdateInstalled(entry) {
|
||||
entry.classList.add('update-done');
|
||||
entry.classList.remove('can-update', 'updatable');
|
||||
$('.update-note', entry).textContent = t('updateCompleted');
|
||||
renderUpdatesOnlyFilter();
|
||||
}
|
Loading…
Reference in New Issue
Block a user