Răsfoiți Sursa

fix #2412: force-update with deps via right-click

tophf 1 lună în urmă
părinte
comite
589bfcd135

+ 3 - 0
src/_locales/en/messages.yml

@@ -1076,6 +1076,9 @@ updateListedCmd:
 updateScript:
   description: Button to update one script.
   message: Update
+updateScriptForced:
+  description: Tooltip for the update icon.
+  message: Right-click or long touch to force-update including dependencies
 updateScriptsAll:
   description: Command/button to update all scripts.
   message: Update all scripts

+ 13 - 6
src/background/utils/db.js

@@ -738,6 +738,9 @@ export async function fetchResources(script, src) {
   if (isRemote(icon)) {
     jobs.push([S_CACHE, icon, ICON_TIMEOUT]);
   }
+  if (!jobs.length) {
+    return;
+  }
   for (let i = 0, type, url, timeout, res; i < jobs.length; i++) {
     [type, url, timeout] = jobs[i];
     if (!(res = pendingDeps[type][url])) {
@@ -758,12 +761,15 @@ export async function fetchResources(script, src) {
   const errors = await Promise.all(jobs);
   const error = errors.map(formatHttpError)::trueJoin('\n');
   if (error) {
-    const message = i18n('msgErrorFetchingResource');
+    let message = i18n('msgErrorFetchingResource');
     sendCmd('UpdateScript', {
       update: { error, message },
       where: { id: getPropsId(script) },
     });
-    return `${message}\n${error}`;
+    message += '\n' + error;
+    return src.force
+      ? { script, text: message }
+      : message;
   }
 }
 
@@ -774,6 +780,7 @@ export async function fetchResources(script, src) {
  * @return {Promise<?>}
  */
 async function fetchResource(src, type, url) {
+  let res;
   if (!isRemote(url)
   || src.update
   || await storage[type].getOne(url) == null) {
@@ -782,12 +789,12 @@ async function fetchResource(src, type, url) {
     try {
       await storage[type].fetch(url, src[FETCH_OPTS]);
     } catch (err) {
-      return err;
-    } finally {
-      if (portId) postToPort(depsPorts, portId, [url, true]);
-      delete pendingDeps[type][url];
+      res = err;
     }
+    if (portId) postToPort(depsPorts, portId, [url, true]);
   }
+  delete pendingDeps[type][url];
+  return res;
 }
 
 function postToPort(ports, id, msg) {

+ 1 - 1
src/background/utils/icon.js

@@ -255,7 +255,7 @@ export function handleHotkeyOrMenu(id, tab) {
     commands.CheckUpdate();
   } else if (id === 'updateScriptsInTab') {
     id = badges[tab.id]?.[IDS];
-    if (id) commands.CheckUpdate([...id]);
+    if (id) commands.CheckUpdate({ ids: [...id] });
   } else if (id.startsWith(KEY_SHOW_BADGE)) {
     setOption(KEY_SHOW_BADGE, id.slice(KEY_SHOW_BADGE.length + 1));
   }

+ 33 - 16
src/background/utils/update.js

@@ -1,5 +1,5 @@
 import {
-  compareVersion, ensureArray, getScriptName, getScriptUpdateUrl, i18n, sendCmd, trueJoin,
+  compareVersion, getScriptName, getScriptUpdateUrl, i18n, sendCmd, trueJoin,
 } from '@/common';
 import {
   __CODE, FETCH_OPTS, METABLOCK_RE, NO_CACHE, TIMEOUT_24HOURS, TIMEOUT_MAX,
@@ -24,32 +24,33 @@ hookOptions(changes => 'autoUpdate' in changes && autoUpdate());
 
 addOwnCommands({
   /**
-   * @param {number | number[] | 'auto'} [id] - when omitted, all scripts are checked
+   * @param {{}} [_]
+   * @param {number[]} [_.ids] - when omitted, all scripts are checked
+   * @param {boolean} [_.auto] - scheduled auto update
+   * @param {boolean} [_.force] - force (ignore checks)
    * @return {Promise<number>} number of updated scripts
    */
-  async CheckUpdate(id) {
-    const isAuto = id === AUTO;
-    const isAll = isAuto || !id;
-    const scripts = isAll ? getScripts() : ensureArray(id).map(getScriptById).filter(Boolean);
+  async CheckUpdate({ ids, force, [AUTO]: auto } = {}) {
+    const isAll = auto || !ids;
+    const scripts = isAll ? getScripts() : ids.map(getScriptById).filter(Boolean);
     const urlOpts = {
       all: true,
       allowedOnly: isAll,
       enabledOnly: isAll && getOption(kUpdateEnabledScriptsOnly),
     };
     const opts = {
+      force,
       [FETCH_OPTS]: {
         ...NO_CACHE,
-        [MULTI]: isAuto ? AUTO : isAll,
+        [MULTI]: auto ? AUTO : isAll,
       },
     };
     const jobs = scripts.map(script => {
       const curId = script.props.id;
       const urls = getScriptUpdateUrl(script, urlOpts);
-      return urls && (
-        processes[curId] || (
-          processes[curId] = doCheckUpdate(curId, script, urls, opts)
-        )
-      );
+      return urls
+        ? processes[curId] ??= doCheckUpdate(curId, script, urls, opts)
+        : force && fetchResources(script, { update: {}, ...opts });
     }).filter(Boolean);
     const results = await Promise.all(jobs);
     const notes = results.filter(r => r?.text);
@@ -65,6 +66,15 @@ addOwnCommands({
     if (isAll) setOption('lastUpdate', Date.now());
     return results.reduce((num, r) => num + (r === true), 0);
   },
+  /**
+   * @param {{ id: number } & VMScriptSourceOptions} opts
+   * @return {Promise<?string>}
+   */
+  UpdateDeps: opts => fetchResources(getScriptById(opts.id), {
+    [FETCH_OPTS]: { ...NO_CACHE },
+    update: {},
+    ...opts
+  }),
 });
 
 async function doCheckUpdate(id, script, urls, opts) {
@@ -106,6 +116,10 @@ async function downloadUpdate(script, urls, opts) {
   const result = { update, where: { id } };
   announce(i18n('msgCheckingForUpdate'));
   try {
+    if (opts.force) {
+      announceUpdate();
+      return (await requestNewer(downloadURL || updateURL, opts)).data;
+    }
     const { data } = await requestNewer(updateURL, { ...FAST_CHECK, ...opts }) || {};
     const { version, [__CODE]: metaStr } = data ? parseMeta(data, { retMetaStr: true }) : {};
     if (compareVersion(meta.version, version) >= 0) {
@@ -117,11 +131,10 @@ async function downloadUpdate(script, urls, opts) {
       announce(i18n('msgUpdated'));
       return data;
     } else {
-      announce(i18n('msgUpdating'));
-      errorMessage = i18n('msgErrorFetchingScript');
+      announceUpdate();
       return downloadURL === updateURL && metaStr.trim() !== data.trim()
         ? data
-        : (await requestNewer(downloadURL, { ...NO_CACHE, ...opts })).data;
+        : (await requestNewer(downloadURL, opts)).data;
     }
   } catch (error) {
     if (process.env.DEBUG) console.error(error);
@@ -137,6 +150,10 @@ async function downloadUpdate(script, urls, opts) {
     });
     sendCmd('UpdateScript', result);
   }
+  function announceUpdate() {
+    announce(i18n('msgUpdating'));
+    errorMessage = i18n('msgErrorFetchingScript');
+  }
 }
 
 function canNotify(script) {
@@ -152,7 +169,7 @@ function autoUpdate() {
   let elapsed = Date.now() - getOption('lastUpdate');
   if (elapsed >= interval) {
     // Wait on startup for things to settle and after unsuspend for network reconnection
-    setTimeout(commands.CheckUpdate, 20e3, AUTO);
+    setTimeout(commands.CheckUpdate, 20e3, { [AUTO]: true });
     elapsed = 0;
   }
   clearTimeout(autoUpdate.timer);

+ 8 - 2
src/common/ui/externals.vue

@@ -39,13 +39,13 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onActivated, onDeactivated, ref, watchEffect } from 'vue';
+import { computed, nextTick, onActivated, onDeactivated, ref, watch, watchEffect } from 'vue';
 import { dataUri2text, formatByteLength, getFullUrl, i18n, makeDataUri, sendCmdDirectly }
   from '@/common';
 import VmCode from '@/common/ui/code';
 import { focusMe, hasKeyModifiers } from '@/common/ui/index';
 
-const props = defineProps(['value', 'cmOptions', 'commands', 'install']);
+const props = defineProps(['value', 'cmOptions', 'commands', 'install', 'updatedDep']);
 const $body = ref();
 const $code = ref();
 const $list = ref();
@@ -104,6 +104,12 @@ onActivated(() => {
 onDeactivated(() => {
   isActive.value = false;
 });
+watch(() => props.updatedDep, url => {
+  const [, currentUrl] = all.value[index.value];
+  const deps = dependencies.value;
+  deps[0 + url] = deps[1 + url] = null;
+  if (url === currentUrl) update();
+});
 watchEffect(update);
 
 async function update() {

+ 53 - 6
src/options/views/edit/index.vue

@@ -5,9 +5,14 @@
         <div
           v-for="(label, navKey) in navItems" :key="navKey"
           class="edit-nav-item" :class="{active: nav === navKey}"
-          v-text="label"
           @click="nav = navKey"
-        />
+        >{{
+          label
+        }}<template v-if="navKey === EXTERNALS">
+            <a @click.stop="onUpdateDeps" class="nav-icon"><icon name="refresh"/></a>
+            <span v-text="depsProgress"/>
+          </template>
+        </div>
       </nav>
       <div class="edit-name text-center ellipsis flex-1">
         <span class="subtle" v-if="script.config.removed" v-text="i18n('headerRecycleBin') + ' / '"/>
@@ -70,8 +75,9 @@
     />
     <vm-externals
       class="flex-auto"
-      v-else-if="nav === 'externals'"
+      v-else-if="nav === EXTERNALS"
       :value="script"
+      :updatedDep
     />
     <vm-help
       class="edit-body"
@@ -95,10 +101,11 @@
 </template>
 
 <script>
+import Icon from '@/common/ui/icon';
 import IconCopy from '~icons/mdi/content-copy';
 import IconPaste from '~icons/mdi/content-paste';
 import {
-  browserWindows,
+  browserWindows, getUniqId,
   debounce, formatByteLength, getScriptName, getScriptUpdateUrl, i18n, isEmpty,
   nullBool2string, sendCmdDirectly, trueJoin,
 } from '@/common';
@@ -114,6 +121,7 @@ import {
   kOrigInclude, kOrigMatch, kUpdateURL,
 } from '../../utils';
 
+const EXTERNALS = 'externals';
 const CUSTOM_PROPS = {
   [kName]: '',
   [kHomepageURL]: '',
@@ -177,6 +185,7 @@ let disposeList;
 let savedCopy;
 let shouldSavePositionOnSave;
 let toggleUnloadSentry;
+let portId, depsDone, depsTotal;
 
 const emit = defineEmits(['close']);
 const props = defineProps({
@@ -197,6 +206,8 @@ const commands = {
   save,
   close,
 };
+const depsProgress = ref('');
+const updatedDep = ref('');
 const hotkeys = ref();
 const errors = ref();
 const errorsLinks = computed(() => {
@@ -237,7 +248,7 @@ const navItems = computed(() => {
     ...id && {
       values: i18n('editNavValues') + (size ? ` (${formatByteLength(size)})` : ''),
     },
-    ...(req || res) && { externals: [req, res]::trueJoin('/') },
+    ...(req || res) && { [EXTERNALS]: [req, res]::trueJoin('/') },
     help: '?',
   };
 });
@@ -312,6 +323,7 @@ onDeactivated(() => {
   store.title = null;
   toggleUnloadSentry(false);
   disposeList?.forEach(dispose => dispose());
+  chrome.runtime.onConnect.removeListener(onUpdateDepsProgress);
 });
 
 function clipboardCopy() {
@@ -427,6 +439,31 @@ function onScript(scr) {
   onChange();
   if (!config.removed) savedCopy = deepCopy(scr);
 }
+async function onUpdateDeps() {
+  depsDone = depsTotal = 0;
+  chrome.runtime.onConnect.addListener(onUpdateDepsProgress);
+  const err = await sendCmdDirectly('UpdateDeps', {
+    id: script.value.props.id,
+    portId: portId = getUniqId(),
+  });
+  if (err) throw new Error(err);
+}
+function onUpdateDepsProgress(port) {
+  if (port.name !== portId) return;
+  port.onMessage.addListener(([url, done]) => {
+    if (done) {
+      ++depsDone;
+      updatedDep.value = url;
+    } else {
+      ++depsTotal;
+    }
+    if (depsDone === depsTotal) {
+      port.disconnect();
+    }
+    depsProgress.value = ` ${depsDone}/${depsTotal}`;
+  });
+}
+
 /** @param {chrome.windows.Window} [wnd] */
 async function savePosition(wnd) {
   if (options.get('editorWindow')) {
@@ -482,7 +519,9 @@ function setupSavePosition({ id: curWndId, tabs }) {
   }
   &-nav-item {
     display: inline-block;
-    padding: 8px 16px;
+    $navPadX: 16px;
+    $navPadY: 8px;
+    padding: $navPadY $navPadX;
     cursor: pointer;
     &.active {
       background: var(--bg);
@@ -492,6 +531,14 @@ function setupSavePosition({ id: curWndId, tabs }) {
       background: var(--fill-0-5);
       box-shadow: 0 -1px 1px var(--fill-4);
     }
+    a.nav-icon {
+      vertical-align: middle;
+      padding: $navPadY $navPadX;
+      margin-right: -$navPadX;
+      &:not(:hover) {
+        color: inherit;
+      }
+    }
   }
   .edit-externals {
     --border: 0;

+ 14 - 3
src/options/views/script-item.vue

@@ -95,7 +95,9 @@
           </tooltip>
           <tooltip
             :disabled="!canUpdate || script.checking"
-            :content="i18n('updateScript')"
+            :content="i18n('updateScript') + ' | ' + i18nUpdateScriptForced"
+            :title="!canUpdate && canUpdateDeps ? i18nUpdateScriptForced : null"
+            v-on="{ contextmenu: (canUpdate || canUpdateDeps) && !script.checking && onUpdate }"
             align="start">
             <a
               class="btn-ghost"
@@ -156,6 +158,7 @@ import { isInput, keyboardService, toggleTip } from '@/common/keyboard';
 import { kDescription, store, TOGGLE_OFF, TOGGLE_ON } from '../utils';
 
 const itemMargin = 8;
+const i18nUpdateScriptForced = i18n('updateScriptForced');
 const visitedRecently = new Set();
 const refreshVisited = () => { store.now = Date.now(); };
 const scheduleRefreshVisited = () => setInterval(refreshVisited, 60e3);
@@ -202,6 +205,12 @@ const author = computed(() => {
   };
 });
 const canUpdate = computed(() => props.script.$canUpdate);
+const notDataUrlRe = /^(?!data:)/;
+const canUpdateDeps = computed(() => {
+  const { meta } = props.script;
+  return meta.require.some(notDataUrlRe.test, notDataUrlRe)
+    || Object.values(meta.resources).some(notDataUrlRe.test, notDataUrlRe);
+});
 const description = computed(() => {
   return props.script.custom[kDescription] || getLocaleString(props.script.meta, kDescription);
 });
@@ -246,10 +255,12 @@ const onRemove = () => emitScript('remove');
 const onRestore = () => emitScript('restore');
 const onTagClick = item => emit('clickTag', item);
 const onToggle = () => emitScript('toggle');
-const onUpdate = async () => {
+const onUpdate = async evt => {
+  evt.preventDefault(); // for contextmenu
   if (props.script.$canUpdate !== -1
   || await showConfirmation(i18n('confirmManualUpdate'))) {
-    emitScript('update');
+    (evt = [props.script]).force = evt.type !== 'click';
+    emit('update', evt);
   }
 };
 /**

+ 4 - 1
src/options/views/tab-installed.vue

@@ -603,7 +603,10 @@ function handleActionToggle(script) {
  */
 async function handleActionUpdate(what, el) {
   if (el) (el = (el.querySelector('svg') || el.closest('svg') || el).classList).add('rotate');
-  await sendCmdDirectly('CheckUpdate', what && ensureArray(what).map(s => s.props.id));
+  await sendCmdDirectly('CheckUpdate', !!what && {
+    ids: ensureArray(what).map(s => s.props.id),
+    force: what.force,
+  });
   el?.remove('rotate');
 }
 function handleClickTag(tag) {

+ 2 - 2
src/popup/views/app.vue

@@ -482,10 +482,10 @@ function onRemoveScript() {
   sendCmdDirectly('MarkRemoved', { id, removed });
 }
 function onUpdateScript() {
-  sendCmdDirectly('CheckUpdate', extras.value.data.props.id);
+  sendCmdDirectly('CheckUpdate', { ids: [extras.value.data.props.id] });
 }
 function onUpdateListed() {
-  sendCmdDirectly('CheckUpdate', Object.keys(store.updatableScripts).map(Number));
+  sendCmdDirectly('CheckUpdate', { ids: Object.keys(store.updatableScripts).map(Number) });
 }
 async function onExclude() {
   const item = extras.value;

+ 1 - 0
src/types.d.ts

@@ -244,6 +244,7 @@ declare interface VMScriptSourceOptions extends DeepPartial<Omit<VMScript, 'infe
 
   bumpDate?: boolean;
   fetchOpts?: object;
+  force?: boolean;
   message?: string;
   portId?: string;
   reloadTab?: boolean;