sourcemonkey 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The small square "copy this to the clipboard" button that sits
|
|
2
|
+
// beside a path.
|
|
3
|
+
//
|
|
4
|
+
// Shared because it appears in more than one place — the viewer's
|
|
5
|
+
// Location row and each entry in its Local overrides row, and the
|
|
6
|
+
// delete dialog's file path — and they must not drift into different
|
|
7
|
+
// icons, different flash durations, or different failure behavior.
|
|
8
|
+
// Its CSS (`.copy-btn`) is in styles.css for the same reason.
|
|
9
|
+
//
|
|
10
|
+
// The text is supplied as a callback rather than a string: a button
|
|
11
|
+
// can outlive the render that made it (the Location row's is one
|
|
12
|
+
// long-lived element moved between renders, so an in-flight "Copied"
|
|
13
|
+
// flash survives a repaint), and it must copy what the row says NOW,
|
|
14
|
+
// not what it said when the button was built.
|
|
15
|
+
// The copy / check glyphs. Both live in the shared sprite
|
|
16
|
+
// (src/glyph-sprite.svg) and are referenced here rather than inlined,
|
|
17
|
+
// so every icon in the extension is defined in one place. Safe to
|
|
18
|
+
// embed via innerHTML: no interpolation, and the strings ship as part
|
|
19
|
+
// of the extension bundle.
|
|
20
|
+
//
|
|
21
|
+
// Both are always in the button; `.copied` on the button swaps which
|
|
22
|
+
// one is displayed, so the flash needs no re-render.
|
|
23
|
+
const COPY_ICON_SVG = '<svg class="copy-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sm-copy"/></svg>';
|
|
24
|
+
const CHECK_ICON_SVG = '<svg class="check-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sm-check"/></svg>';
|
|
25
|
+
// How long the checkmark stays up after a successful copy.
|
|
26
|
+
const COPIED_FLASH_MS = 1200;
|
|
27
|
+
export function makeCopyButton(options) {
|
|
28
|
+
const btn = document.createElement('button');
|
|
29
|
+
btn.type = 'button';
|
|
30
|
+
btn.className = 'copy-btn';
|
|
31
|
+
btn.title = options.label;
|
|
32
|
+
btn.setAttribute('aria-label', options.label);
|
|
33
|
+
btn.innerHTML = COPY_ICON_SVG + CHECK_ICON_SVG;
|
|
34
|
+
// Per-button timer, so two buttons flashing at once don't cancel each
|
|
35
|
+
// other, and a rapid double-click on one leaves no stuck checkmark:
|
|
36
|
+
// each click clears its own prior reset and schedules a new one.
|
|
37
|
+
let resetTimer = null;
|
|
38
|
+
btn.addEventListener('click', () => {
|
|
39
|
+
void (async () => {
|
|
40
|
+
const text = options.text();
|
|
41
|
+
if (!text)
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
await navigator.clipboard.writeText(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// The write can be rejected (permission denied, or no user
|
|
48
|
+
// gesture credited). The text is selectable on the page, so
|
|
49
|
+
// there is nothing to fall back to and nothing worth saying.
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// `.copied` swaps the visible icon to a checkmark via CSS.
|
|
53
|
+
btn.classList.add('copied');
|
|
54
|
+
if (resetTimer !== null)
|
|
55
|
+
clearTimeout(resetTimer);
|
|
56
|
+
resetTimer = setTimeout(() => {
|
|
57
|
+
btn.classList.remove('copied');
|
|
58
|
+
resetTimer = null;
|
|
59
|
+
}, COPIED_FLASH_MS);
|
|
60
|
+
})();
|
|
61
|
+
});
|
|
62
|
+
return btn;
|
|
63
|
+
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
// The "make a local copy of this script" dialog, opened from the Local
|
|
2
|
+
// copy button on the viewer's Collection row.
|
|
3
|
+
//
|
|
4
|
+
// It owns the whole action, the way the move dialog owns Move: the
|
|
5
|
+
// file name and header fields, the refusals, the write through
|
|
6
|
+
// `makeLocalCopy`, and any failure in its own error slot — so the
|
|
7
|
+
// caller only ever hears about a copy that landed.
|
|
8
|
+
//
|
|
9
|
+
// See docs/local-copy.md.
|
|
10
|
+
import { checkExistingFiles, COPY_OVER_ITSELF, defaultCopyName, dependencyFiles, downloadsCopiesDir, DOWNLOADS_COPIES_NAME, DOWNLOADS_COPIES_SUBDIR, findDownloadsCollection, isCopyOfItself, makeLocalCopy, planFiles, readDownloadsCopiesSetting, validateCopyName, } from './local-copy.js';
|
|
11
|
+
import { effectiveDisplayName } from './collection-display.js';
|
|
12
|
+
import { isFileSchemeAccessAllowed, renderFileAccessBanner } from './file-access.js';
|
|
13
|
+
import { formatBytes } from '../script-display.js';
|
|
14
|
+
import { collectionUsesLocalFiles, describeError, getCollections, } from './script-store.js';
|
|
15
|
+
import { getValue } from '../userscript-header.js';
|
|
16
|
+
import { localEditVersion } from '../version-compare.js';
|
|
17
|
+
import { dlog } from '../debug-log.js';
|
|
18
|
+
let elements = null;
|
|
19
|
+
// One at a time, claimed synchronously — same reason as the other
|
|
20
|
+
// dialogs' guards: two overlapping opens would leave the first caller's
|
|
21
|
+
// promise unsettled and `showModal` throwing on the second.
|
|
22
|
+
let dialogInFlight = false;
|
|
23
|
+
function el(tag, className, text) {
|
|
24
|
+
const node = document.createElement(tag);
|
|
25
|
+
if (className)
|
|
26
|
+
node.className = className;
|
|
27
|
+
if (text !== undefined)
|
|
28
|
+
node.textContent = text;
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
// A one-line box that grows with its content, to the cap the CSS sets
|
|
32
|
+
// (four lines) and then scrolls. A `<textarea>` rather than an
|
|
33
|
+
// `<input>` because the value can be a sentence, but it still holds ONE
|
|
34
|
+
// line of a metadata block — see `sanitizeOneLine`.
|
|
35
|
+
function growingInput(id) {
|
|
36
|
+
const box = document.createElement('textarea');
|
|
37
|
+
box.id = id;
|
|
38
|
+
box.rows = 1;
|
|
39
|
+
box.spellcheck = false;
|
|
40
|
+
return box;
|
|
41
|
+
}
|
|
42
|
+
// Fit the box to its content. Height is cleared first so shrinking
|
|
43
|
+
// works: `scrollHeight` never reports less than the current height.
|
|
44
|
+
function autoGrow(box) {
|
|
45
|
+
box.style.height = 'auto';
|
|
46
|
+
box.style.height = `${box.scrollHeight}px`;
|
|
47
|
+
}
|
|
48
|
+
// A metadata value is one line by definition, so a pasted newline
|
|
49
|
+
// becomes a space rather than a broken header. Same length, so the
|
|
50
|
+
// caret doesn't move.
|
|
51
|
+
function sanitizeOneLine(box) {
|
|
52
|
+
if (box.value.includes('\n') || box.value.includes('\r')) {
|
|
53
|
+
box.value = box.value.replace(/[\r\n]/g, ' ');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function textInput(id) {
|
|
57
|
+
const input = el('input');
|
|
58
|
+
input.type = 'text';
|
|
59
|
+
input.id = id;
|
|
60
|
+
input.spellcheck = false;
|
|
61
|
+
input.autocomplete = 'off';
|
|
62
|
+
return input;
|
|
63
|
+
}
|
|
64
|
+
function buildDialog() {
|
|
65
|
+
const dialog = el('dialog', 'sm-dialog copy-dialog');
|
|
66
|
+
dialog.appendChild(el('h2', 'dlg-title', 'Make a local copy of this script'));
|
|
67
|
+
// The file-access banner, when the toggle is off: a copy that can be
|
|
68
|
+
// written but never read back would register as a broken script.
|
|
69
|
+
const banner = el('div', 'dlg-banner');
|
|
70
|
+
dialog.appendChild(banner);
|
|
71
|
+
const body = el('div', 'dlg-body');
|
|
72
|
+
// One tooltip per row, on the label AND the control: the label is
|
|
73
|
+
// what a reader points at to ask "what is this?", and the control is
|
|
74
|
+
// where the pointer already is when they wonder.
|
|
75
|
+
const row = (label, control, tooltip, note) => {
|
|
76
|
+
const cell = el('span', 'dlg-label', label);
|
|
77
|
+
// A second line under the label, for what the field accepts that
|
|
78
|
+
// its name doesn't say.
|
|
79
|
+
if (note)
|
|
80
|
+
cell.appendChild(el('span', 'dlg-label-note', note));
|
|
81
|
+
cell.title = tooltip;
|
|
82
|
+
control.title = tooltip;
|
|
83
|
+
body.appendChild(cell);
|
|
84
|
+
body.appendChild(control);
|
|
85
|
+
};
|
|
86
|
+
const target = el('span', 'dlg-static');
|
|
87
|
+
target.id = 'copy-target';
|
|
88
|
+
row('Target collection', target, 'Copy to this collection. It uses a directory under Chrome\u2019s Downloads directory.');
|
|
89
|
+
const fileName = textInput('copy-file-name');
|
|
90
|
+
row('Filename', fileName, 'Copy script here, relative to the collection\u2019s directory. '
|
|
91
|
+
+ 'The relative path can include a subdirectory.', '(with subdir)');
|
|
92
|
+
const depsWrap = el('div', 'dlg-deps');
|
|
93
|
+
const depsLabel = el('label', 'dlg-check');
|
|
94
|
+
const deps = el('input');
|
|
95
|
+
deps.type = 'checkbox';
|
|
96
|
+
deps.id = 'copy-deps';
|
|
97
|
+
const depsText = el('span');
|
|
98
|
+
depsLabel.append(deps, depsText);
|
|
99
|
+
depsWrap.appendChild(depsLabel);
|
|
100
|
+
const depsHint = el('div', 'dlg-hint');
|
|
101
|
+
depsWrap.appendChild(depsHint);
|
|
102
|
+
const depsList = el('ul', 'dlg-deps-list');
|
|
103
|
+
depsWrap.appendChild(depsList);
|
|
104
|
+
// The row's own tooltip is about the paths it lists; the checkbox
|
|
105
|
+
// decides whether anything is written there, which is a different
|
|
106
|
+
// question, so it carries its own.
|
|
107
|
+
row('Dependencies', depsWrap, 'Local paths for @require and @resource files this script loads. '
|
|
108
|
+
+ 'Files at these local paths will be used if they exist.');
|
|
109
|
+
depsLabel.title = 'Also save local copies of @require and @resource files if checked.';
|
|
110
|
+
// The header fields are a group of their own: everything above says
|
|
111
|
+
// where the copy goes, everything below is what will differ from the
|
|
112
|
+
// script it is copied from.
|
|
113
|
+
const heading = el('div', 'dlg-section', 'Updated headers in downloaded script');
|
|
114
|
+
body.appendChild(heading);
|
|
115
|
+
const name = el('span', 'dlg-static');
|
|
116
|
+
name.id = 'copy-name';
|
|
117
|
+
row('Name', name, 'The @name for the copied script. Unchanged, to keep a link to the source script.');
|
|
118
|
+
// `<original> → [new]`, so the edit is read against what it replaces
|
|
119
|
+
// rather than having to be remembered.
|
|
120
|
+
const versionWrap = el('div', 'dlg-arrow');
|
|
121
|
+
const versionWas = el('span', 'dlg-was');
|
|
122
|
+
versionWas.id = 'copy-version-was';
|
|
123
|
+
// Its own, overriding the row's: this half is the version being
|
|
124
|
+
// copied FROM, where the box beside it is the one being written.
|
|
125
|
+
versionWas.title = 'The @version of the source script';
|
|
126
|
+
const version = textInput('copy-version');
|
|
127
|
+
versionWrap.append(versionWas, el('span', 'dlg-arrow-mark', '\u2192'), version);
|
|
128
|
+
row('Version', versionWrap, 'The @version for the source and copied script. '
|
|
129
|
+
+ 'The copied script gets a "-local" suffix by default.');
|
|
130
|
+
const description = growingInput('copy-description');
|
|
131
|
+
row('Description', description, 'The @description for the copied script');
|
|
132
|
+
dialog.appendChild(body);
|
|
133
|
+
// In the shared error row, so the text lines up with the form's
|
|
134
|
+
// padding rather than the dialog's edge.
|
|
135
|
+
const errorRow = el('div', 'dlg-error-row');
|
|
136
|
+
const error = el('p', 'dlg-error');
|
|
137
|
+
error.setAttribute('role', 'alert');
|
|
138
|
+
errorRow.appendChild(error);
|
|
139
|
+
dialog.appendChild(errorRow);
|
|
140
|
+
const actions = el('div', 'dlg-actions');
|
|
141
|
+
const cancel = el('button', undefined, 'Cancel');
|
|
142
|
+
cancel.type = 'button';
|
|
143
|
+
const ok = el('button', 'primary', 'Copy');
|
|
144
|
+
ok.type = 'button';
|
|
145
|
+
ok.id = 'copy-confirm-btn';
|
|
146
|
+
// Enter in the description would insert a line the header can't hold,
|
|
147
|
+
// and it is what a one-line field is expected to submit on. Wired
|
|
148
|
+
// here rather than at the field, so it can name the button directly.
|
|
149
|
+
description.addEventListener('keydown', (ev) => {
|
|
150
|
+
if (ev.key !== 'Enter')
|
|
151
|
+
return;
|
|
152
|
+
ev.preventDefault();
|
|
153
|
+
ok.click();
|
|
154
|
+
});
|
|
155
|
+
actions.append(cancel, ok);
|
|
156
|
+
dialog.appendChild(actions);
|
|
157
|
+
document.body.appendChild(dialog);
|
|
158
|
+
return {
|
|
159
|
+
dialog, banner, target, fileName, deps, depsLabel, depsText, depsList, depsHint,
|
|
160
|
+
name, versionWas, version, description, error, cancel, ok,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Open the dialog and resolve with the new copy, or null when it was
|
|
164
|
+
// dismissed or nothing was written.
|
|
165
|
+
export function openCopyDialog(request) {
|
|
166
|
+
if (!elements)
|
|
167
|
+
elements = buildDialog();
|
|
168
|
+
const ui = elements;
|
|
169
|
+
if (dialogInFlight)
|
|
170
|
+
return Promise.resolve(null);
|
|
171
|
+
dialogInFlight = true;
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
try {
|
|
174
|
+
runDialog(ui, request, (result) => {
|
|
175
|
+
dialogInFlight = false;
|
|
176
|
+
resolve(result);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
dialogInFlight = false;
|
|
181
|
+
resolve(null);
|
|
182
|
+
dlog(`[SourceMonkey] copy dialog failed to open: ${describeError(err)}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function readOpenContext() {
|
|
187
|
+
const [setting, collections, fileAccess] = await Promise.all([
|
|
188
|
+
readDownloadsCopiesSetting(),
|
|
189
|
+
getCollections(),
|
|
190
|
+
isFileSchemeAccessAllowed(),
|
|
191
|
+
]);
|
|
192
|
+
return {
|
|
193
|
+
copiesDir: setting ? downloadsCopiesDir(setting.downloadsDir) : null,
|
|
194
|
+
collection: findDownloadsCollection(setting, collections),
|
|
195
|
+
fileAccess,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function runDialog(ui, request, resolve) {
|
|
199
|
+
const { script } = request;
|
|
200
|
+
let settled = false;
|
|
201
|
+
// A submit is in flight. Esc is refused for its duration: closing
|
|
202
|
+
// would resolve the caller `null` while the files are still landing,
|
|
203
|
+
// so the caller would skip the exclusive-enable sweep and the
|
|
204
|
+
// navigation the new copy needs.
|
|
205
|
+
let submitting = false;
|
|
206
|
+
// The open-time reads have landed. Until then nothing can be checked
|
|
207
|
+
// against the disk, so the confirm stays off.
|
|
208
|
+
let context = null;
|
|
209
|
+
// The last COMPLETED disk check, with the file name it was made for.
|
|
210
|
+
// Everything about replacing — the warning line, the marks in the
|
|
211
|
+
// dependency list, the confirm's wording — is drawn from this and
|
|
212
|
+
// never from a half-known state: an edit leaves the last answer on
|
|
213
|
+
// screen and disables the confirm until the new one lands, so the
|
|
214
|
+
// marks and the button don't blink off and back on with every
|
|
215
|
+
// keystroke. `null` until the first check.
|
|
216
|
+
let checked = null;
|
|
217
|
+
// A check is pending or in flight, so what is on screen is the
|
|
218
|
+
// PREVIOUS answer. Only the confirm reacts.
|
|
219
|
+
let checking = false;
|
|
220
|
+
let checkTimer;
|
|
221
|
+
let checkSeq = 0;
|
|
222
|
+
const finish = (result) => {
|
|
223
|
+
if (settled)
|
|
224
|
+
return;
|
|
225
|
+
settled = true;
|
|
226
|
+
if (checkTimer !== undefined)
|
|
227
|
+
clearTimeout(checkTimer);
|
|
228
|
+
if (ui.dialog.open)
|
|
229
|
+
ui.dialog.close();
|
|
230
|
+
resolve(result);
|
|
231
|
+
};
|
|
232
|
+
// ----- Fill the form from the script -----
|
|
233
|
+
const header = script.header;
|
|
234
|
+
const originalVersion = header ? getValue(header, 'version') ?? '' : '';
|
|
235
|
+
ui.name.textContent = (header && getValue(header, 'name')) || '(no @name)';
|
|
236
|
+
// `<none>` rather than an empty gap: the arrow needs a left-hand
|
|
237
|
+
// side, and "the script declares no version" is what it says.
|
|
238
|
+
ui.versionWas.textContent = originalVersion || '<none>';
|
|
239
|
+
ui.version.value = originalVersion ? localEditVersion(originalVersion) : '';
|
|
240
|
+
ui.version.placeholder = originalVersion ? '' : 'none';
|
|
241
|
+
ui.description.value = header ? getValue(header, 'description') ?? '' : '';
|
|
242
|
+
ui.description.placeholder = ui.description.value ? '' : 'none';
|
|
243
|
+
ui.fileName.value = defaultCopyName(script);
|
|
244
|
+
const deps = dependencyFiles(script);
|
|
245
|
+
// Off to begin with; `paintDependencies` runs again when the
|
|
246
|
+
// open-time reads land and must not undo a tick made in between.
|
|
247
|
+
ui.deps.checked = false;
|
|
248
|
+
paintDependencies(ui, deps, null);
|
|
249
|
+
ui.target.replaceChildren();
|
|
250
|
+
ui.target.appendChild(el('span', 'dlg-target-name', DOWNLOADS_COPIES_NAME));
|
|
251
|
+
const targetHint = el('span', 'dlg-hint', ` (a ${DOWNLOADS_COPIES_SUBDIR} directory under Chrome's Downloads directory)`);
|
|
252
|
+
ui.target.appendChild(targetHint);
|
|
253
|
+
ui.banner.replaceChildren();
|
|
254
|
+
// ----- Validation -----
|
|
255
|
+
const currentPlan = () => ({
|
|
256
|
+
fileName: ui.fileName.value.trim(),
|
|
257
|
+
version: ui.version.value,
|
|
258
|
+
description: ui.description.value,
|
|
259
|
+
includeDependencies: ui.deps.checked && !ui.deps.disabled,
|
|
260
|
+
});
|
|
261
|
+
// What the last completed check says the copy would overwrite. The
|
|
262
|
+
// checkbox is read live rather than checked for: the check covers
|
|
263
|
+
// every file the plan COULD write, so ticking it changes what will
|
|
264
|
+
// be replaced without needing another listing.
|
|
265
|
+
const replaceState = () => {
|
|
266
|
+
if (!checked)
|
|
267
|
+
return { scriptExists: false, depsExisting: 0 };
|
|
268
|
+
const copyingDeps = ui.deps.checked && !ui.deps.disabled;
|
|
269
|
+
return {
|
|
270
|
+
scriptExists: checked.existing.includes(checked.fileName),
|
|
271
|
+
depsExisting: copyingDeps
|
|
272
|
+
? checked.existing.filter((p) => p !== checked.fileName).length
|
|
273
|
+
: 0,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
// Paint the error slot and the confirm button from the current state.
|
|
277
|
+
// `report` is what the last attempt said, which outranks the live
|
|
278
|
+
// validation until the user changes something.
|
|
279
|
+
let report = null;
|
|
280
|
+
const sync = () => {
|
|
281
|
+
const nameError = validateCopyName(ui.fileName.value)
|
|
282
|
+
?? (context?.copiesDir && isCopyOfItself(script, context.copiesDir, ui.fileName.value)
|
|
283
|
+
? COPY_OVER_ITSELF
|
|
284
|
+
: null);
|
|
285
|
+
const message = report ?? nameError;
|
|
286
|
+
const blocked = !context || !context.fileAccess || nameError !== null || report !== null;
|
|
287
|
+
const { scriptExists, depsExisting } = replaceState();
|
|
288
|
+
const willReplace = scriptExists || depsExisting > 0;
|
|
289
|
+
// Where the dependencies WOULD go, when they aren't going: still
|
|
290
|
+
// worth reading, but not a description of what Copy is about to
|
|
291
|
+
// do, so it and the list it heads are dimmed.
|
|
292
|
+
const copyingDeps = ui.deps.checked && !ui.deps.disabled;
|
|
293
|
+
ui.depsHint.classList.toggle('dimmed', !copyingDeps);
|
|
294
|
+
ui.depsList.classList.toggle('dimmed', !copyingDeps);
|
|
295
|
+
ui.error.textContent = message ?? replaceWarning(scriptExists, depsExisting);
|
|
296
|
+
ui.error.classList.toggle('warn', !message && willReplace);
|
|
297
|
+
// Paths and marks both from the last completed check, so a name
|
|
298
|
+
// being typed can't pair a new path with the old answer about it.
|
|
299
|
+
paintDependencyRows(ui, checked?.existing ?? [], checked?.fileName ?? '');
|
|
300
|
+
ui.fileName.classList.toggle('invalid', nameError !== null);
|
|
301
|
+
// A pending check is the one thing that disables the confirm
|
|
302
|
+
// without changing anything else on screen: the answer it would
|
|
303
|
+
// act on is about to be replaced, and it may well come back the
|
|
304
|
+
// same.
|
|
305
|
+
ui.ok.disabled = blocked || submitting || checking;
|
|
306
|
+
ui.ok.textContent = willReplace ? 'Replace' : 'Copy';
|
|
307
|
+
};
|
|
308
|
+
// The disk check: which of the files this plan would write are
|
|
309
|
+
// already there. Debounced behind typing, and sequenced so a slow
|
|
310
|
+
// listing can't overwrite the answer for a newer name.
|
|
311
|
+
const scheduleCheck = () => {
|
|
312
|
+
report = null;
|
|
313
|
+
checking = true;
|
|
314
|
+
// Bumped HERE, not only in `runCheck`: a check already awaiting a
|
|
315
|
+
// listing must not be allowed to land its answer for the name the
|
|
316
|
+
// user has just moved off. Without this it would, then clear
|
|
317
|
+
// `checking` and re-enable the confirm — so a click in the 250ms
|
|
318
|
+
// before this edit's own check ran would write the new name with
|
|
319
|
+
// the old name's answer about what is already there.
|
|
320
|
+
checkSeq++;
|
|
321
|
+
if (checkTimer !== undefined)
|
|
322
|
+
clearTimeout(checkTimer);
|
|
323
|
+
checkTimer = setTimeout(() => void runCheck(), 250);
|
|
324
|
+
sync();
|
|
325
|
+
};
|
|
326
|
+
const runCheck = async () => {
|
|
327
|
+
checkTimer = undefined;
|
|
328
|
+
const seq = ++checkSeq;
|
|
329
|
+
const fileName = ui.fileName.value.trim();
|
|
330
|
+
if (!context?.copiesDir
|
|
331
|
+
|| validateCopyName(ui.fileName.value) !== null
|
|
332
|
+
|| isCopyOfItself(script, context.copiesDir, ui.fileName.value)) {
|
|
333
|
+
// Nothing to check against — and the name is what the confirm is
|
|
334
|
+
// already refusing, so the previous answer stays on screen rather
|
|
335
|
+
// than being cleared to a state nobody can act on.
|
|
336
|
+
checking = false;
|
|
337
|
+
sync();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Every file the plan COULD write, dependencies included, so the
|
|
341
|
+
// list's marks don't depend on the checkbox.
|
|
342
|
+
const files = planFiles(script, { ...currentPlan(), includeDependencies: true });
|
|
343
|
+
try {
|
|
344
|
+
const found = await checkExistingFiles(context.copiesDir, files);
|
|
345
|
+
if (seq !== checkSeq || settled)
|
|
346
|
+
return;
|
|
347
|
+
checked = { fileName, existing: found.existing };
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
if (seq !== checkSeq || settled)
|
|
351
|
+
return;
|
|
352
|
+
dlog(`[SourceMonkey] copy dialog could not check existing files: ${describeError(err)}`);
|
|
353
|
+
}
|
|
354
|
+
checking = false;
|
|
355
|
+
sync();
|
|
356
|
+
};
|
|
357
|
+
ui.fileName.oninput = scheduleCheck;
|
|
358
|
+
// No re-check: the check covers every file the plan COULD write, so
|
|
359
|
+
// the box only changes which of them count. `report` is cleared like
|
|
360
|
+
// any other edit — otherwise a failed copy's message would sit there
|
|
361
|
+
// blocking the confirm until some other field was touched.
|
|
362
|
+
ui.deps.onchange = () => { report = null; sync(); };
|
|
363
|
+
ui.version.oninput = () => { report = null; sync(); };
|
|
364
|
+
ui.description.oninput = () => {
|
|
365
|
+
sanitizeOneLine(ui.description);
|
|
366
|
+
autoGrow(ui.description);
|
|
367
|
+
report = null;
|
|
368
|
+
sync();
|
|
369
|
+
};
|
|
370
|
+
// Replaced on every open, so stale closures can't fire.
|
|
371
|
+
ui.cancel.onclick = () => finish(null);
|
|
372
|
+
ui.dialog.onclose = () => finish(null);
|
|
373
|
+
ui.dialog.oncancel = (event) => {
|
|
374
|
+
if (submitting)
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
};
|
|
377
|
+
ui.ok.onclick = () => void submit();
|
|
378
|
+
const submit = async () => {
|
|
379
|
+
if (!context)
|
|
380
|
+
return;
|
|
381
|
+
submitting = true;
|
|
382
|
+
sync();
|
|
383
|
+
ui.cancel.disabled = true;
|
|
384
|
+
const spinner = el('span', 'busy-spinner');
|
|
385
|
+
ui.ok.after(spinner);
|
|
386
|
+
try {
|
|
387
|
+
const result = await makeLocalCopy({
|
|
388
|
+
script,
|
|
389
|
+
plan: currentPlan(),
|
|
390
|
+
overwrite: (() => {
|
|
391
|
+
const { scriptExists, depsExisting } = replaceState();
|
|
392
|
+
return scriptExists || depsExisting > 0;
|
|
393
|
+
})(),
|
|
394
|
+
});
|
|
395
|
+
finish(result);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
report = describeError(err);
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
submitting = false;
|
|
402
|
+
spinner.remove();
|
|
403
|
+
ui.cancel.disabled = false;
|
|
404
|
+
sync();
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
report = null;
|
|
408
|
+
checking = true;
|
|
409
|
+
sync();
|
|
410
|
+
ui.dialog.showModal();
|
|
411
|
+
// After `showModal`: a closed dialog is `display: none`, where a box
|
|
412
|
+
// has no scroll height to measure.
|
|
413
|
+
autoGrow(ui.description);
|
|
414
|
+
ui.fileName.focus();
|
|
415
|
+
ui.fileName.setSelectionRange(0, Math.max(0, ui.fileName.value.length - '.user.js'.length));
|
|
416
|
+
// ----- The open-time reads, after the dialog is on screen -----
|
|
417
|
+
void readOpenContext().then((ctx) => {
|
|
418
|
+
if (settled)
|
|
419
|
+
return;
|
|
420
|
+
context = ctx;
|
|
421
|
+
if (ctx.copiesDir) {
|
|
422
|
+
targetHint.textContent = ` (${ctx.copiesDir})`;
|
|
423
|
+
if (ctx.collection) {
|
|
424
|
+
ui.target.firstChild.textContent = effectiveDisplayName(ctx.collection);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
paintDependencies(ui, deps, ctx.collection);
|
|
428
|
+
if (!ctx.fileAccess) {
|
|
429
|
+
ui.banner.replaceChildren(renderFileAccessBanner('then open this dialog again'));
|
|
430
|
+
}
|
|
431
|
+
void runCheck();
|
|
432
|
+
}, (err) => {
|
|
433
|
+
if (settled)
|
|
434
|
+
return;
|
|
435
|
+
report = `Could not read the collections: ${describeError(err)}`;
|
|
436
|
+
sync();
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// What Replace would overwrite, as the line in the error slot. Which
|
|
440
|
+
// files: a dependency that is already there is marked in the list under
|
|
441
|
+
// the checkbox, which already names every file, so the line names
|
|
442
|
+
// none — and it counts dependencies only when they are being copied.
|
|
443
|
+
function replaceWarning(scriptExists, depsExisting) {
|
|
444
|
+
if (scriptExists && depsExisting > 0) {
|
|
445
|
+
return 'Target script file and dependency files already exist and will be replaced';
|
|
446
|
+
}
|
|
447
|
+
if (scriptExists)
|
|
448
|
+
return 'Target script file already exists and will be replaced';
|
|
449
|
+
if (depsExisting === 1)
|
|
450
|
+
return 'Target dependency file already exists and will be replaced';
|
|
451
|
+
if (depsExisting > 1)
|
|
452
|
+
return 'Target dependency files already exist and will be replaced';
|
|
453
|
+
return '';
|
|
454
|
+
}
|
|
455
|
+
function paintDependencyRows(ui, existing, fileName) {
|
|
456
|
+
// Dependencies go under the SCRIPT's folder, so a file name with a
|
|
457
|
+
// subdirectory in it moves them too — the list shows where each one
|
|
458
|
+
// would actually land, collection-relative, like the field above it.
|
|
459
|
+
const dir = fileName.includes('/') ? fileName.slice(0, fileName.lastIndexOf('/') + 1) : '';
|
|
460
|
+
const set = new Set(existing);
|
|
461
|
+
for (const li of ui.depsList.querySelectorAll('li')) {
|
|
462
|
+
const dep = dir + (li.dataset.dep ?? '');
|
|
463
|
+
li.replaceChildren(dep);
|
|
464
|
+
if (set.has(dep))
|
|
465
|
+
li.appendChild(el('span', 'dlg-exists', ' (already exists)'));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// The dependencies row: the checkbox's wording, whether it can be
|
|
469
|
+
// ticked at all, and the list under it.
|
|
470
|
+
function paintDependencies(ui, deps, collection) {
|
|
471
|
+
const total = deps.reduce((n, d) => n + d.size, 0);
|
|
472
|
+
const noun = deps.length === 1 ? 'dependency file' : 'dependency files';
|
|
473
|
+
const localFilesOff = collection !== null && !collectionUsesLocalFiles(collection);
|
|
474
|
+
if (deps.length === 0) {
|
|
475
|
+
ui.depsText.textContent = 'No dependency files to copy';
|
|
476
|
+
ui.depsHint.textContent = '';
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
ui.depsText.textContent = `Also copy ${deps.length} ${noun} (${formatBytes(total)})`;
|
|
480
|
+
ui.depsHint.textContent = localFilesOff
|
|
481
|
+
? 'Local files are turned off for this collection, so copies would not be used'
|
|
482
|
+
: 'Local filenames in download collection';
|
|
483
|
+
}
|
|
484
|
+
const off = deps.length === 0 || localFilesOff;
|
|
485
|
+
ui.deps.disabled = off;
|
|
486
|
+
// Only ever forced off, never on: this runs a second time when the
|
|
487
|
+
// open-time reads land, and a tick made before then is the user's.
|
|
488
|
+
if (off)
|
|
489
|
+
ui.deps.checked = false;
|
|
490
|
+
ui.depsLabel.classList.toggle('off', off);
|
|
491
|
+
ui.depsList.replaceChildren(...deps.map((d) => {
|
|
492
|
+
// Path relative to the SCRIPT's folder. `paintDependencyRows`
|
|
493
|
+
// renders it against the current file name and adds the
|
|
494
|
+
// "(already exists)" mark.
|
|
495
|
+
const li = el('li', undefined, d.relativePath);
|
|
496
|
+
// No tooltip of its own, so the row's applies: the declared URL
|
|
497
|
+
// this file came from says nothing about the local path the row
|
|
498
|
+
// is showing.
|
|
499
|
+
li.dataset.dep = d.relativePath;
|
|
500
|
+
return li;
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Which installed copy of a script is preferred, when only one of them
|
|
2
|
+
// should run or be shown: the order behind "Enable all" picking one
|
|
3
|
+
// copy per script in a collection, and the toolbar popup picking the
|
|
4
|
+
// copy that stands for a script none of whose copies is active.
|
|
5
|
+
//
|
|
6
|
+
// Pure, so popup-view.ts can use it in the service worker and the
|
|
7
|
+
// rules can be unit tested. Design: docs/script-enablement.md.
|
|
8
|
+
import { compareScriptNames } from '../script-display.js';
|
|
9
|
+
import { compareVersions } from '../version-compare.js';
|
|
10
|
+
function isActive(c) {
|
|
11
|
+
return c.enabled && c.collectionOn;
|
|
12
|
+
}
|
|
13
|
+
// Most recently active first: an active copy before any inactive one
|
|
14
|
+
// (the later `activeSince` first between two active), then by when it
|
|
15
|
+
// went inactive. A copy with no recorded times gives no signal and
|
|
16
|
+
// comes last.
|
|
17
|
+
function compareRecency(a, b) {
|
|
18
|
+
const ra = isActive(a);
|
|
19
|
+
const rb = isActive(b);
|
|
20
|
+
if (ra !== rb)
|
|
21
|
+
return ra ? -1 : 1;
|
|
22
|
+
const ta = (ra ? a.activeSince : a.inactiveSince) ?? 0;
|
|
23
|
+
const tb = (rb ? b.activeSince : b.inactiveSince) ?? 0;
|
|
24
|
+
return tb - ta;
|
|
25
|
+
}
|
|
26
|
+
// The order between copies in one collection: most recently active, then
|
|
27
|
+
// the highest `@version` (a copy without one last), then location.
|
|
28
|
+
export function compareInCollection(a, b) {
|
|
29
|
+
const recency = compareRecency(a, b);
|
|
30
|
+
if (recency !== 0)
|
|
31
|
+
return recency;
|
|
32
|
+
if (a.version !== b.version) {
|
|
33
|
+
if (a.version === null)
|
|
34
|
+
return 1;
|
|
35
|
+
if (b.version === null)
|
|
36
|
+
return -1;
|
|
37
|
+
const v = compareVersions(b.version, a.version);
|
|
38
|
+
if (v !== 0)
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
return a.location < b.location ? -1 : a.location > b.location ? 1 : 0;
|
|
42
|
+
}
|
|
43
|
+
// How close a copy is to active: in an enabled collection (so only its
|
|
44
|
+
// own box is off), ticked in a disabled collection, unticked in one.
|
|
45
|
+
function closeness(c) {
|
|
46
|
+
if (c.collectionOn)
|
|
47
|
+
return 0;
|
|
48
|
+
return c.enabled ? 1 : 2;
|
|
49
|
+
}
|
|
50
|
+
// The order between copies anywhere: closest to active, then most
|
|
51
|
+
// recently active, then collection name, then `compareInCollection`.
|
|
52
|
+
export function comparePreferred(a, b) {
|
|
53
|
+
return closeness(a) - closeness(b)
|
|
54
|
+
|| compareRecency(a, b)
|
|
55
|
+
|| compareScriptNames(a.collectionName, b.collectionName)
|
|
56
|
+
|| compareInCollection(a, b);
|
|
57
|
+
}
|
|
58
|
+
// The first of `copies` by `compare`, or undefined when there are none.
|
|
59
|
+
export function preferredCopy(copies, facts, compare = comparePreferred) {
|
|
60
|
+
let best;
|
|
61
|
+
for (const copy of copies) {
|
|
62
|
+
if (best === undefined || compare(facts(copy), facts(best)) < 0)
|
|
63
|
+
best = copy;
|
|
64
|
+
}
|
|
65
|
+
return best;
|
|
66
|
+
}
|