stylus/manage/updater-ui.js

303 lines
10 KiB
JavaScript
Raw Normal View History

/* global $ $$ $create messageBoxProxy scrollElementIntoView */// dom.js
/* global $entry */// render.js
/* global API */// msg.js
/* global filterAndAppend filtersSelector */// filters.js
/* global newUI */// manage.js
/* global sorter */
/* global t */// localization.js
2017-08-15 11:40:36 +00:00
'use strict';
$('#check-all-updates').onclick = checkUpdateAll;
$('#check-all-updates-force').onclick = checkUpdateAll;
$('#apply-all-updates').onclick = applyUpdateAll;
$('#update-history').onclick = showUpdateHistory;
2017-08-15 11:40:36 +00:00
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');
2018-01-01 17:02:49 +00:00
const btnCheck = $('#check-all-updates');
const btnCheckForce = $('#check-all-updates-force');
const btnApply = $('#apply-all-updates');
const noUpdates = $('#update-all-no-updates');
btnCheck.disabled = true;
btnCheckForce.classList.add('hidden');
btnApply.classList.add('hidden');
noUpdates.classList.add('hidden');
2017-08-15 11:40:36 +00:00
const ignoreDigest = this && this.id === 'check-all-updates-force';
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
2018-01-01 17:02:49 +00:00
.map(checkUpdate);
2017-08-15 11:40:36 +00:00
let total = 0;
let checked = 0;
let skippedEdited = 0;
let updated = 0;
2018-01-01 17:02:49 +00:00
chrome.runtime.onConnect.addListener(function onConnect(port) {
if (port.name !== 'updater') return;
port.onMessage.addListener(observer);
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updater.checkAllStyles({
2018-01-01 17:02:49 +00:00
save: false,
observe: true,
ignoreDigest,
});
2017-08-15 11:40:36 +00:00
function observer(info, port) {
2018-01-01 17:02:49 +00:00
if ('count' in info) {
total = info.count;
}
if (info.updated) {
if (++updated === 1) {
btnApply.disabled = true;
}
btnApply.dataset.value = updated;
}
if (info.updated || 'error' in info) {
2018-01-01 17:02:49 +00:00
checked++;
skippedEdited += !info.updated && [info.STATES.EDITED, info.STATES.MAYBE_EDITED].includes(info.error);
2018-01-01 17:02:49 +00:00
reportUpdateState(info);
2017-08-15 11:40:36 +00:00
}
const progress = $('#update-progress');
const maxWidth = progress.parentElement.clientWidth;
progress.style.width = Math.round(checked / total * maxWidth) + 'px';
2018-02-16 09:49:53 +00:00
if (!info.done) return;
port.onMessage.removeListener(observer);
2017-08-15 11:40:36 +00:00
document.body.classList.remove('update-in-progress');
2018-01-01 17:02:49 +00:00
btnCheck.disabled = total === 0;
btnApply.disabled = false;
2017-08-15 11:40:36 +00:00
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
if (!updated) {
2018-01-01 17:02:49 +00:00
noUpdates.dataset.skippedEdited = skippedEdited > 0;
noUpdates.classList.remove('hidden');
btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
2017-08-15 11:40:36 +00:00
}
}
}
2018-01-01 17:02:49 +00:00
function checkUpdate(entry, {single} = {}) {
2017-08-15 11:40:36 +00:00
$('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', entry).title = '';
if (single) {
API.updater.checkStyle({
2017-08-15 11:40:36 +00:00
save: false,
2018-01-01 17:02:49 +00:00
id: entry.styleId,
2017-08-15 11:40:36 +00:00
ignoreDigest: entry.classList.contains('update-problem'),
2018-01-01 17:02:49 +00:00
}).then(reportUpdateState);
2017-08-15 11:40:36 +00:00
}
entry.classList.remove('checking-update', 'no-update', 'update-problem');
entry.classList.add('checking-update');
}
2018-01-01 17:02:49 +00:00
function reportUpdateState({updated, style, error, STATES}) {
const isCheckAll = document.body.classList.contains('update-in-progress');
const entry = $entry(style);
2018-01-01 04:45:04 +00:00
const newClasses = new Map([
/*
When a style is updated/installed, handleUpdateInstalled() clears "updatable"
and sets "update-done" class (optionally "install-done").
If you don't close the manager and the style is changed remotely,
checking for updates would find an update so we need to ensure the entry is "updatable"
*/
['updatable', true],
// falsy = remove
['checking-update', 0],
['update-done', 0],
['install-done', 0],
['no-update', 0],
['update-problem', 0],
]);
2018-01-01 17:02:49 +00:00
if (updated) {
newClasses.set('can-update', true);
entry.updatedCode = style;
$('.update-note', entry).textContent = '';
$('#only-updates').classList.remove('hidden');
} else if (!entry.classList.contains('can-update')) {
const same = (
error === STATES.SAME_MD5 ||
error === STATES.SAME_CODE ||
error === STATES.SAME_VERSION
);
const edited = error === STATES.EDITED || error === STATES.MAYBE_EDITED;
entry.dataset.error = error;
if (!error) {
error = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl;
} else if (typeof error === 'number') {
error = t('updateCheckFailBadResponseCode', [error]) + '\n' + style.updateUrl;
} else if (error === STATES.EDITED) {
error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} else if (error === STATES.MAYBE_EDITED) {
error = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} else if (typeof error === 'object' && error.message) {
// UserCSS meta errors provide an object
error = error.message;
2018-01-01 17:02:49 +00:00
}
const message = same ? t('updateCheckSucceededNoUpdate') : error;
newClasses.set('no-update', true);
newClasses.set('update-problem', !same);
$('.update-note', entry).textContent = message;
$('.check-update', entry).title = newUI.enabled ? message : '';
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
// digest may change silently when forcing an update of a locally edited style
// so we need to update it in entry's styleMeta in all open manager tabs
if (error === STATES.SAME_CODE) {
for (const view of chrome.extension.getViews({type: 'tab'})) {
if (view.location.pathname === location.pathname) {
const entry = $entry(style, view.document);
if (entry) entry.styleMeta.originalDigest = style.originalDigest;
}
}
}
if (!isCheckAll) {
2018-01-01 17:02:49 +00:00
renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
2017-08-15 11:40:36 +00:00
}
}
2018-01-01 04:45:04 +00:00
// construct a new className:
// 1. add all truthy newClasses
// 2. remove falsy newClasses
// 3. keep existing classes otherwise
const classes = new Map([...entry.classList.values()].map(cls => [cls, true]));
for (const [cls, newState] of newClasses) {
2018-01-01 17:02:49 +00:00
classes.set(cls, newState);
}
const className = [...classes]
2018-01-01 17:02:49 +00:00
.map(([cls, state]) => state && cls)
.filter(Boolean)
.join(' ');
if (className !== entry.className) {
entry.className = className;
}
2018-01-01 04:45:04 +00:00
if (filtersSelector.hide && isCheckAll) {
filterAndAppend({entry}).then(sorter.updateStripes);
} else if (updated && !isCheckAll) {
renderUpdatesOnlyFilter();
2017-08-15 11:40:36 +00:00
}
}
function renderUpdatesOnlyFilter({show, check} = {}) {
const numUpdatable = $$('.can-update').length;
const mightUpdate = numUpdatable > 0 || $('.update-problem');
const checkbox = $('#only-updates input');
2017-08-15 11:40:36 +00:00
show = show !== undefined ? show : mightUpdate;
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
$('#only-updates').classList.toggle('hidden', !show);
checkbox.checked = check && show;
2017-08-15 11:40:36 +00:00
checkbox.dispatchEvent(new Event('change'));
const btnApply = $('#apply-all-updates');
btnApply.classList.toggle('hidden', !numUpdatable);
btnApply.dataset.value = numUpdatable;
2017-08-15 11:40:36 +00:00
}
async function showUpdateHistory(event) {
event.preventDefault();
const log = $create('.update-history-log');
let scroller, toggler;
let deleted = false;
await require(['/js/storage-util']); /* global chromeLocal */
const [lines, states] = await Promise.all([
2018-01-01 17:02:49 +00:00
chromeLocal.getValue('updateLog'),
API.updater.getStates(),
]);
const logText = lines.join('\n');
messageBoxProxy.show({
title: t('updateCheckHistory'),
contents: log,
blockScroll: true,
buttons: [
t('confirmOK'),
logText && {textContent: t('confirmDelete'), onclick: deleteHistory},
],
onshow: logText && (() => {
scroller = $('#message-box-contents');
scroller.tabIndex = 0;
setTimeout(() => scroller.focus());
scrollToBottom();
$('#message-box-buttons button').insertAdjacentElement('afterend',
// TODO: add a global class for our labels
// TODO: add a <template> or a common function to create such controls
$create('label', {style: 'position: relative; padding-left: 16px;'}, [
toggler =
$create('input', {type: 'checkbox', checked: true, onchange: toggleSkipped}),
$create('SVG:svg.svg-icon.checked',
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
t('manageOnlyUpdates'),
]));
toggler.rxRemoveNOP = new RegExp(
'^[^#]*(' +
Object.keys(states)
.filter(k => k.startsWith('SAME_'))
.map(k => states[k])
.join('|') +
').*\r?\n', 'gm');
toggler.onchange();
}),
2017-08-15 11:40:36 +00:00
});
function scrollToBottom() {
scroller.scrollTop = 1e9;
}
function calcScrollRatio() {
return (scroller.scrollTop + scroller.clientHeight) / scroller.scrollHeight;
}
function toggleSkipped() {
if (deleted) {
return;
}
const scrollRatio = calcScrollRatio();
2018-01-01 17:02:49 +00:00
log.textContent = !this.checked ? logText : logText.replace(this.rxRemoveNOP, '');
if (Math.abs(scrollRatio - calcScrollRatio()) > .1) {
scroller.scrollTop = scrollRatio * scroller.scrollHeight - scroller.clientHeight;
}
}
function deleteHistory() {
if (deleted) {
2018-01-01 17:02:49 +00:00
chromeLocal.setValue('updateLog', logText.split('\n'));
setTimeout(scrollToBottom);
} else {
2018-01-01 17:02:49 +00:00
chromeLocal.remove('updateLog');
log.textContent = '';
}
deleted = !deleted;
toggler.onchange();
this.textContent = t(deleted ? 'undo' : 'confirmDelete');
}
2017-08-15 11:40:36 +00:00
}
/* exported handleUpdateInstalled */
function handleUpdateInstalled(entry, reason) {
const isNew = reason === 'install';
const note = t(isNew ? 'installButtonInstalled' : 'updateCompleted');
entry.classList.add('update-done', ...(isNew ? ['install-done'] : []));
2017-08-15 11:40:36 +00:00
entry.classList.remove('can-update', 'updatable');
$('.update-note', entry).textContent = note;
$('.updated', entry).title = note;
2017-08-15 11:40:36 +00:00
renderUpdatesOnlyFilter();
}