/* global messageBox UI filtersSelector filterAndAppend sorter $ $$ $create API onDOMready scrollElementIntoView t chromeLocal */ /* exported handleUpdateInstalled */ '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'); // 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'); const ignoreDigest = this && this.id === 'check-all-updates-force'; $$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)')) .map(checkUpdate); let total = 0; let checked = 0; let skippedEdited = 0; let updated = 0; chrome.runtime.onConnect.addListener(function onConnect(port) { if (port.name !== 'updater') return; port.onMessage.addListener(observer); chrome.runtime.onConnect.removeListener(onConnect); }); API.updateCheckAll({ save: false, observe: true, ignoreDigest, }); function observer(info, port) { 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) { checked++; skippedEdited += !info.updated && [info.STATES.EDITED, info.STATES.MAYBE_EDITED].includes(info.error); reportUpdateState(info); } const progress = $('#update-progress'); const maxWidth = progress.parentElement.clientWidth; progress.style.width = Math.round(checked / total * maxWidth) + 'px'; if (!info.done) return; port.onMessage.removeListener(observer); document.body.classList.remove('update-in-progress'); // btnCheck.disabled = total === 0; btnApply.disabled = false; renderUpdatesOnlyFilter({check: updated + skippedEdited > 0}); if (!updated) { noUpdates.dataset.skippedEdited = skippedEdited > 0; noUpdates.classList.remove('hidden'); btnCheckForce.classList.toggle('hidden', skippedEdited === 0); } } } function checkUpdate(entry, {single} = {}) { $('.check-update', entry).dataset.title = t('checkingForUpdate'); if (single) { API.updateCheck({ save: false, id: entry.styleId, ignoreDigest: entry.classList.contains('update-problem'), }).then(reportUpdateState); } entry.classList.remove('checking-update', 'no-update', 'update-problem'); entry.classList.add('checking-update'); } function reportUpdateState({updated, style, error, STATES}) { const isCheckAll = document.body.classList.contains('update-in-progress'); const entry = $(UI.ENTRY_ID_PREFIX + style.id); 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], ]); if (updated) { newClasses.set('can-update', true); entry.updatedCode = style; $('#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; } const message = same ? t('updateCheckSucceededNoUpdate') : error; newClasses.set('no-update', true); newClasses.set('update-problem', !same); $('.check-update', entry).dataset.title = UI.enabled ? message : ''; $('.update', entry).dataset.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 = view.$(UI.ENTRY_ID_PREFIX + style.id); if (entry) entry.styleMeta.originalDigest = style.originalDigest; } } } if (!isCheckAll) { renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')}); } } // 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.entries()) { classes.set(cls, newState); } const className = [...classes.entries()] .map(([cls, state]) => state && cls) .filter(Boolean) .join(' '); if (className !== entry.className) { entry.className = className; } if (filtersSelector.hide && isCheckAll) { filterAndAppend({entry}).then(sorter.updateStripes); } else if (updated && !isCheckAll) { renderUpdatesOnlyFilter(); } } function renderUpdatesOnlyFilter({show, check} = {}) { const numUpdatable = $$('.can-update').length; const mightUpdate = numUpdatable > 0 || $('.update-problem'); const checkbox = $('#only-updates input'); show = show !== undefined ? show : mightUpdate; check = check !== undefined ? show && check : checkbox.checked && mightUpdate; $('#only-updates').classList.toggle('hidden', !show); checkbox.checked = check && show; checkbox.dispatchEvent(new Event('change')); const btnApply = $('#apply-all-updates'); btnApply.classList.toggle('hidden', !numUpdatable); btnApply.dataset.value = numUpdatable; } function showUpdateHistory(event) { event.preventDefault(); const log = $create('.update-history-log'); let logText, scroller, toggler; let deleted = false; Promise.all([ chromeLocal.getValue('updateLog'), API.getUpdaterStates(), ]).then(([lines = [], states]) => { logText = lines.join('\n'); messageBox({ 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