/* global messageBox */ /* global ENTRY_ID_PREFIX, newUI */ /* global filtersSelector, filterAndAppend, sorter */ '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, }).then(done); function observer(info) { if ('count' in info) { total = info.count; } if (info.updated) { if (++updated === 1) { btnApply.disabled = true; btnApply.classList.remove('hidden'); } btnApply.dataset.value = updated; } if (info.updated || info.error) { checked++; skippedEdited += [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'; } function done() { 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} = {}) { $('.update-note', entry).textContent = t('checkingForUpdate'); $('.check-update', entry).title = ''; 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 entry = $(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; $('.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'); } 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'); 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')}); } } // 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) { filterAndAppend({entry}); sorter.update(); } } 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'); if (!btnApply.matches('.hidden')) { if (numUpdatable > 0) { btnApply.dataset.value = numUpdatable; } else { btnApply.classList.add('hidden'); } } } 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