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,504 @@
|
|
|
1
|
+
// Options page controller. Owns the actual configurable settings
|
|
2
|
+
// (link capture, debug-logging gate, local-file-override search roots,
|
|
3
|
+
// GitHub auth token), plus a read-only storage-usage table. The
|
|
4
|
+
// per-collection management UI lives on `collections.html` /
|
|
5
|
+
// `collections.ts`; a header link points users back and forth.
|
|
6
|
+
import { DEBUG_LOGGING_KEY, installDebugLoggingListener, loadDebugLoggingFlag, } from '../debug-log.js';
|
|
7
|
+
import { GITHUB_AUTH_TOKEN_KEY } from '../github-auth.js';
|
|
8
|
+
import { INSTALL_CAPTURE_KEY } from './install-capture.js';
|
|
9
|
+
import { LOCAL_ROOTS_KEY, parseLocalRootLines, readRootsSetting, rootsFromText, textFromRoots, } from '../local-file-overrides.js';
|
|
10
|
+
import { fetchFileUrlText } from './directories.js';
|
|
11
|
+
import { describeError, getCollections } from './script-store.js';
|
|
12
|
+
import { updateCollection, writeSetting } from './worker-client.js';
|
|
13
|
+
import { timeStorageRead } from './storage-scan.js';
|
|
14
|
+
import { formatQuota, formatStorageUsage } from '../script-display.js';
|
|
15
|
+
import { summarizeLocalStorage, toWholeKb, withRemainder, } from './storage-usage.js';
|
|
16
|
+
import { collectionSupportsLocalFiles } from '../local-file-overrides.js';
|
|
17
|
+
import { isFileSchemeAccessAllowed, renderFileAccessBanner } from './file-access.js';
|
|
18
|
+
import { DOWNLOADS_COPIES_KEY, parseDownloadsCopiesSetting, readDownloadsCopiesSetting, } from './local-copy.js';
|
|
19
|
+
import { installHomeLinkHandler, onNamedTabFocus, openNamedTab } from './page-links.js';
|
|
20
|
+
const openCollectionsBtn = document.getElementById('open-collections');
|
|
21
|
+
// The collections page is a named tab: focus the open one if there is
|
|
22
|
+
// one, and otherwise open a new tab rather than replacing this one.
|
|
23
|
+
openCollectionsBtn.addEventListener('click', () => {
|
|
24
|
+
void openNamedTab('collections');
|
|
25
|
+
});
|
|
26
|
+
// The header bar's icon and product name are the same link.
|
|
27
|
+
installHomeLinkHandler();
|
|
28
|
+
// A link elsewhere focusing this tab rather than opening a second one.
|
|
29
|
+
// This page has no anchors of its own to land on, so the whole of it is
|
|
30
|
+
// the target: back to the top. See src/extension/page-links.ts.
|
|
31
|
+
onNamedTabFocus('options', () => {
|
|
32
|
+
scrollTo({ top: 0, behavior: 'smooth' });
|
|
33
|
+
});
|
|
34
|
+
// Both boolean settings differ only in their storage key and what an
|
|
35
|
+
// absent value means, so they share one binder. `defaultValue` is what
|
|
36
|
+
// an unset key reads back as — capture is opt-out, debug logging is
|
|
37
|
+
// opt-in.
|
|
38
|
+
initBooleanSetting('capture-userjs', INSTALL_CAPTURE_KEY, true);
|
|
39
|
+
// This page both sets the flag AND reads storage through shared code
|
|
40
|
+
// that logs under it (the storage-read timings in `storage-scan`), so
|
|
41
|
+
// it observes the flag like any other context: each one reads it for
|
|
42
|
+
// itself. Refreshes it asks for run in the worker, which has its own.
|
|
43
|
+
installDebugLoggingListener();
|
|
44
|
+
void loadDebugLoggingFlag();
|
|
45
|
+
initBooleanSetting('debug-logging', DEBUG_LOGGING_KEY, false);
|
|
46
|
+
initGithubTokenControls();
|
|
47
|
+
initLocalRootControls();
|
|
48
|
+
initDownloadsDirControls();
|
|
49
|
+
initStorageUsage();
|
|
50
|
+
// Read-only table of what is stored in `chrome.storage.local`, by
|
|
51
|
+
// kind, against Chrome's quota. Each paint scans every row, so a burst
|
|
52
|
+
// of writes (a refresh, a script spamming GM_setValue) is coalesced into
|
|
53
|
+
// one repaint a second at most.
|
|
54
|
+
function initStorageUsage() {
|
|
55
|
+
const table = document.getElementById('storage-usage');
|
|
56
|
+
const limit = document.getElementById('storage-limit');
|
|
57
|
+
if (!table || !limit)
|
|
58
|
+
return;
|
|
59
|
+
const quota = chrome.storage.local.QUOTA_BYTES;
|
|
60
|
+
if (typeof quota === 'number' && quota > 0) {
|
|
61
|
+
limit.textContent = `Chrome limits each extension to ${formatQuota(quota)} of storage.`;
|
|
62
|
+
limit.hidden = false;
|
|
63
|
+
}
|
|
64
|
+
let pending = false;
|
|
65
|
+
// Paints can overlap (a slow full read, then a change); only the
|
|
66
|
+
// newest one may draw, so an older read finishing last can't put
|
|
67
|
+
// stale numbers back.
|
|
68
|
+
let latest = 0;
|
|
69
|
+
const paint = async () => {
|
|
70
|
+
pending = false;
|
|
71
|
+
const mine = ++latest;
|
|
72
|
+
const usage = await readUsage();
|
|
73
|
+
if (mine === latest)
|
|
74
|
+
table.replaceChildren(...usageRows(usage));
|
|
75
|
+
};
|
|
76
|
+
void paint();
|
|
77
|
+
chrome.storage.onChanged.addListener((_changes, areaName) => {
|
|
78
|
+
if (areaName !== 'local' || pending)
|
|
79
|
+
return;
|
|
80
|
+
pending = true;
|
|
81
|
+
setTimeout(() => void paint(), 1000);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function readUsage() {
|
|
85
|
+
const quota = chrome.storage.local.QUOTA_BYTES;
|
|
86
|
+
if (typeof quota !== 'number' || quota <= 0) {
|
|
87
|
+
return { error: 'Chrome did not report its storage limit' };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const [rows, used] = await Promise.all([
|
|
91
|
+
timeStorageRead('storage usage', () => chrome.storage.local.get(null)),
|
|
92
|
+
chrome.storage.local.getBytesInUse(null),
|
|
93
|
+
]);
|
|
94
|
+
return { rows: withRemainder(summarizeLocalStorage(rows), used), used, quota };
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return { error: `Could not read storage usage: ${describeError(err)}` };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const kbFormat = new Intl.NumberFormat();
|
|
101
|
+
function usageRows(usage) {
|
|
102
|
+
const row = (cells, className) => {
|
|
103
|
+
const tr = document.createElement('tr');
|
|
104
|
+
if (className)
|
|
105
|
+
tr.className = className;
|
|
106
|
+
for (const [text, cls, span] of cells) {
|
|
107
|
+
const td = document.createElement('td');
|
|
108
|
+
td.textContent = text;
|
|
109
|
+
if (cls)
|
|
110
|
+
td.className = cls;
|
|
111
|
+
if (span)
|
|
112
|
+
td.colSpan = span;
|
|
113
|
+
tr.appendChild(td);
|
|
114
|
+
}
|
|
115
|
+
return tr;
|
|
116
|
+
};
|
|
117
|
+
if ('error' in usage)
|
|
118
|
+
return [row([[usage.error]])];
|
|
119
|
+
// Whole KB throughout, so the column visibly adds up to the total.
|
|
120
|
+
const kb = toWholeKb(usage.rows, usage.used);
|
|
121
|
+
return [
|
|
122
|
+
// A row that rounds to nothing is left out rather than shown as 0.
|
|
123
|
+
...usage.rows.flatMap((r, i) => kb[i] === 0 ? [] : [
|
|
124
|
+
row([[r.label], [r.count, 'num'], [`${kbFormat.format(kb[i])} KB`, 'num']]),
|
|
125
|
+
]),
|
|
126
|
+
row([
|
|
127
|
+
['Total'],
|
|
128
|
+
// Spans Count too: in the Size column alone, this longer text
|
|
129
|
+
// would set that column's width and open a wide gap before every
|
|
130
|
+
// size.
|
|
131
|
+
[formatStorageUsage(usage.used, usage.quota), 'num', 2],
|
|
132
|
+
], 'total'),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
// The Downloads folder the local-copy feature learned (see
|
|
136
|
+
// docs/local-copy.md). Editable for the one case the extension can't
|
|
137
|
+
// detect on its own: Chrome's download location changed after the
|
|
138
|
+
// first copy. Saving keeps the recorded collection id — the next copy
|
|
139
|
+
// re-checks whether that collection still sits at the new path.
|
|
140
|
+
function initDownloadsDirControls() {
|
|
141
|
+
const box = document.getElementById('downloads-dir');
|
|
142
|
+
const saveBtn = document.getElementById('downloads-dir-save');
|
|
143
|
+
const status = document.getElementById('downloads-dir-status');
|
|
144
|
+
if (!box || !saveBtn || !status)
|
|
145
|
+
return;
|
|
146
|
+
let saved = null;
|
|
147
|
+
const paint = (setting) => {
|
|
148
|
+
saved = setting;
|
|
149
|
+
if (!box.matches(':focus'))
|
|
150
|
+
box.value = setting?.downloadsDir ?? '';
|
|
151
|
+
saveBtn.disabled = box.value.trim() === (saved?.downloadsDir ?? '');
|
|
152
|
+
};
|
|
153
|
+
void readDownloadsCopiesSetting().then(paint);
|
|
154
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
155
|
+
if (areaName !== 'local')
|
|
156
|
+
return;
|
|
157
|
+
const change = changes[DOWNLOADS_COPIES_KEY];
|
|
158
|
+
if (change)
|
|
159
|
+
paint(parseDownloadsCopiesSetting(change.newValue));
|
|
160
|
+
});
|
|
161
|
+
box.addEventListener('input', () => {
|
|
162
|
+
saveBtn.disabled = box.value.trim() === (saved?.downloadsDir ?? '');
|
|
163
|
+
status.textContent = '';
|
|
164
|
+
});
|
|
165
|
+
saveBtn.addEventListener('click', () => {
|
|
166
|
+
const dir = box.value.trim().replace(/\/+$/, '');
|
|
167
|
+
if (!dir.startsWith('/')) {
|
|
168
|
+
status.textContent = 'The Downloads folder must be an absolute path';
|
|
169
|
+
status.className = 'token-status err';
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
void writeSetting(DOWNLOADS_COPIES_KEY, { downloadsDir: dir, collectionId: saved?.collectionId ?? null })
|
|
173
|
+
.then(() => {
|
|
174
|
+
status.textContent = 'Saved';
|
|
175
|
+
status.className = 'token-status ok';
|
|
176
|
+
}, (err) => {
|
|
177
|
+
status.textContent = `Could not save the Downloads folder: ${describeError(err)}`;
|
|
178
|
+
status.className = 'token-status err';
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function initBooleanSetting(elementId, key, defaultValue) {
|
|
183
|
+
const cb = document.getElementById(elementId);
|
|
184
|
+
if (!cb)
|
|
185
|
+
return;
|
|
186
|
+
const read = (raw) => (typeof raw === 'boolean' ? raw : defaultValue);
|
|
187
|
+
// Hydrate from storage so the box reflects the persisted state, then
|
|
188
|
+
// mirror future writes (from anywhere — DevTools, another options
|
|
189
|
+
// tab) so the two never drift out of sync.
|
|
190
|
+
void chrome.storage.local.get(key).then((got) => {
|
|
191
|
+
cb.checked = read(got[key]);
|
|
192
|
+
});
|
|
193
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
194
|
+
if (areaName !== 'local')
|
|
195
|
+
return;
|
|
196
|
+
const change = changes[key];
|
|
197
|
+
if (!change)
|
|
198
|
+
return;
|
|
199
|
+
cb.checked = read(change.newValue);
|
|
200
|
+
});
|
|
201
|
+
cb.addEventListener('change', () => {
|
|
202
|
+
const wanted = cb.checked;
|
|
203
|
+
writeSetting(key, wanted).catch((err) => {
|
|
204
|
+
console.info(`[SourceMonkey] could not save ${key}:`, describeError(err));
|
|
205
|
+
// Back to what is stored, unless another change has moved it since.
|
|
206
|
+
if (cb.checked === wanted)
|
|
207
|
+
cb.checked = !wanted;
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Local search roots: a multi-line box, one absolute path per line.
|
|
212
|
+
//
|
|
213
|
+
// The setting stores the PARSED array, so this box is where text
|
|
214
|
+
// becomes roots — which is why Save is disabled while any line is
|
|
215
|
+
// malformed. Nothing downstream re-parses, so nothing downstream has to
|
|
216
|
+
// decide what a bad line means.
|
|
217
|
+
//
|
|
218
|
+
// Two kinds of complaint, and only one of them blocks Save:
|
|
219
|
+
// - Format. Decided locally and instantly. Blocks.
|
|
220
|
+
// - Existence. Requires a `file://` read, so it's debounced — and
|
|
221
|
+
// it's a warning: a directory can appear later (a repo you haven't
|
|
222
|
+
// cloned yet), and saving a root that isn't there yet costs
|
|
223
|
+
// nothing, since resolution simply finds no candidate under it.
|
|
224
|
+
function initLocalRootControls() {
|
|
225
|
+
const box = document.getElementById('local-roots-text');
|
|
226
|
+
const saveBtn = document.getElementById('local-roots-save');
|
|
227
|
+
const status = document.getElementById('local-roots-status');
|
|
228
|
+
const problems = document.getElementById('local-roots-problems');
|
|
229
|
+
const fileAccess = document.getElementById('local-roots-file-access');
|
|
230
|
+
if (!box || !saveBtn || !status || !problems || !fileAccess)
|
|
231
|
+
return;
|
|
232
|
+
let savedText = '';
|
|
233
|
+
let checkTimer;
|
|
234
|
+
// Existence results, keyed by canonical root URL. Cached for the
|
|
235
|
+
// page's lifetime so re-validating on each keystroke doesn't re-probe
|
|
236
|
+
// every unchanged line.
|
|
237
|
+
const exists = new Map();
|
|
238
|
+
void chrome.storage.local.get(LOCAL_ROOTS_KEY).then((got) => {
|
|
239
|
+
savedText = textFromRoots(readRootsSetting(got[LOCAL_ROOTS_KEY]));
|
|
240
|
+
// Don't overwrite a user who started typing before the read landed
|
|
241
|
+
// — the same reasoning as the cross-tab guard below, for the much
|
|
242
|
+
// narrower window between page load and hydration.
|
|
243
|
+
if (!box.value)
|
|
244
|
+
box.value = savedText;
|
|
245
|
+
void validate();
|
|
246
|
+
});
|
|
247
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
248
|
+
if (areaName !== 'local')
|
|
249
|
+
return;
|
|
250
|
+
const change = changes[LOCAL_ROOTS_KEY];
|
|
251
|
+
if (!change)
|
|
252
|
+
return;
|
|
253
|
+
const next = textFromRoots(readRootsSetting(change.newValue));
|
|
254
|
+
if (next === savedText)
|
|
255
|
+
return;
|
|
256
|
+
savedText = next;
|
|
257
|
+
// Don't clobber an edit being typed in THIS box with a write from
|
|
258
|
+
// another options tab. Focus is the signal: a box the user is in
|
|
259
|
+
// keeps its text and shows "Unsaved changes" against the new saved
|
|
260
|
+
// value, so nothing is lost silently either way.
|
|
261
|
+
if (box.value !== next && !box.matches(':focus'))
|
|
262
|
+
box.value = next;
|
|
263
|
+
void validate();
|
|
264
|
+
});
|
|
265
|
+
box.addEventListener('input', () => {
|
|
266
|
+
renderProblems();
|
|
267
|
+
if (checkTimer !== undefined)
|
|
268
|
+
clearTimeout(checkTimer);
|
|
269
|
+
checkTimer = setTimeout(() => void validate(), 400);
|
|
270
|
+
});
|
|
271
|
+
saveBtn.addEventListener('click', () => void onSave());
|
|
272
|
+
async function onSave() {
|
|
273
|
+
const roots = rootsFromText(box.value);
|
|
274
|
+
// Save is disabled while this is null, so reaching it means a
|
|
275
|
+
// keystroke landed between the last render and the click.
|
|
276
|
+
if (!roots)
|
|
277
|
+
return;
|
|
278
|
+
// Write back the canonical form: the stored array IS the setting,
|
|
279
|
+
// and showing anything else would leave the box disagreeing with
|
|
280
|
+
// what's actually searched.
|
|
281
|
+
savedText = textFromRoots(roots);
|
|
282
|
+
box.value = savedText;
|
|
283
|
+
saveBtn.disabled = true;
|
|
284
|
+
try {
|
|
285
|
+
await writeSetting(LOCAL_ROOTS_KEY, roots);
|
|
286
|
+
// The roots only change what the NEXT refresh resolves to, so the
|
|
287
|
+
// refresh is part of the save — same reasoning as the collection
|
|
288
|
+
// dialog's local-overrides option, which refreshes its collection
|
|
289
|
+
// on save. Without it a newly-added root would
|
|
290
|
+
// appear to do nothing until something else happened to touch a
|
|
291
|
+
// collection.
|
|
292
|
+
setStatus('Saved, refreshing scripts', '');
|
|
293
|
+
await refreshLocalCollections();
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
saveBtn.disabled = false;
|
|
297
|
+
}
|
|
298
|
+
await validate();
|
|
299
|
+
}
|
|
300
|
+
// Refresh only the collections a root could possibly affect. http(s)
|
|
301
|
+
// collections and the web / GitHub containers resolve everything over
|
|
302
|
+
// the network, so refreshing them would be cost with no possible
|
|
303
|
+
// change.
|
|
304
|
+
async function refreshLocalCollections() {
|
|
305
|
+
try {
|
|
306
|
+
const collections = await getCollections();
|
|
307
|
+
for (const collection of collections) {
|
|
308
|
+
if (!collectionSupportsLocalFiles(collection.path))
|
|
309
|
+
continue;
|
|
310
|
+
await updateCollection(collection.id, 'manual');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// A refresh that fails is not a failed save: the setting is
|
|
315
|
+
// written, and every collection re-resolves on its own next
|
|
316
|
+
// refresh. Per-collection failures already surface on the
|
|
317
|
+
// collections page.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Probe every well-formed root that hasn't been probed yet, then
|
|
321
|
+
// repaint. Cheap after the first pass — `exists` short-circuits.
|
|
322
|
+
async function validate() {
|
|
323
|
+
renderProblems();
|
|
324
|
+
const entries = parseLocalRootLines(box.value);
|
|
325
|
+
// A malformed line already blocks Save, so there is nothing to be
|
|
326
|
+
// learned from probing the others yet — and the format complaint is
|
|
327
|
+
// the one the user has to act on first.
|
|
328
|
+
if (entries.some((e) => e.error))
|
|
329
|
+
return;
|
|
330
|
+
const unknown = entries
|
|
331
|
+
.flatMap((e) => (e.url ? [e.url] : []))
|
|
332
|
+
.filter((url) => !exists.has(url));
|
|
333
|
+
// Before the early return, or a banner raised by an earlier pass
|
|
334
|
+
// outlives the condition that raised it — deleting the offending
|
|
335
|
+
// line would leave it on screen with nothing left to probe.
|
|
336
|
+
if (unknown.length === 0) {
|
|
337
|
+
if (await isFileSchemeAccessAllowed())
|
|
338
|
+
fileAccess.replaceChildren();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// With "Allow access to file URLs" off, every read fails the same
|
|
342
|
+
// way a missing directory does — so say which it is, using the
|
|
343
|
+
// shared banner rather than a second copy of its wording. Reload is
|
|
344
|
+
// the right retry here: `exists` is cached for the page's life.
|
|
345
|
+
if (!(await isFileSchemeAccessAllowed())) {
|
|
346
|
+
if (!fileAccess.firstChild)
|
|
347
|
+
fileAccess.appendChild(renderFileAccessBanner());
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
fileAccess.replaceChildren();
|
|
351
|
+
await Promise.all(unknown.map(async (url) => {
|
|
352
|
+
exists.set(url, await isReadableDir(url));
|
|
353
|
+
}));
|
|
354
|
+
renderProblems();
|
|
355
|
+
}
|
|
356
|
+
function renderProblems() {
|
|
357
|
+
const entries = parseLocalRootLines(box.value);
|
|
358
|
+
problems.replaceChildren();
|
|
359
|
+
for (const entry of entries) {
|
|
360
|
+
const message = problemFor(entry);
|
|
361
|
+
if (!message)
|
|
362
|
+
continue;
|
|
363
|
+
const li = document.createElement('li');
|
|
364
|
+
li.textContent = message;
|
|
365
|
+
problems.appendChild(li);
|
|
366
|
+
}
|
|
367
|
+
renderStatus(entries);
|
|
368
|
+
}
|
|
369
|
+
function problemFor(entry) {
|
|
370
|
+
if (entry.error)
|
|
371
|
+
return `Line ${entry.lineNumber}: ${entry.error}`;
|
|
372
|
+
if (entry.url && exists.get(entry.url) === false) {
|
|
373
|
+
return `Line ${entry.lineNumber}: No directory found at ${entry.text}`;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
function renderStatus(entries) {
|
|
378
|
+
const bad = entries.filter((e) => e.error).length;
|
|
379
|
+
// A format error is the one thing that blocks a save. The listed
|
|
380
|
+
// problem already names the line and the fix, and the button is
|
|
381
|
+
// visibly disabled, so the status line stays out of it rather than
|
|
382
|
+
// restating both.
|
|
383
|
+
saveBtn.disabled = bad > 0;
|
|
384
|
+
if (bad > 0) {
|
|
385
|
+
setStatus('', '');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (textFromRoots(rootsFromText(box.value) ?? []) !== savedText) {
|
|
389
|
+
setStatus('Unsaved changes', '');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (entries.length === 0) {
|
|
393
|
+
setStatus('No directories configured, so only each script\'s own folder and its collection\'s are searched', '');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
setStatus(`Searching ${entries.length} ${entries.length === 1 ? 'directory' : 'directories'} after each script's own folder and its collection's`, 'ok');
|
|
397
|
+
}
|
|
398
|
+
function setStatus(text, cls) {
|
|
399
|
+
status.textContent = text;
|
|
400
|
+
status.classList.remove('ok', 'err');
|
|
401
|
+
if (cls)
|
|
402
|
+
status.classList.add(cls);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// True when the URL names a readable directory. Chrome serves a
|
|
406
|
+
// directory listing for a `file://` URL ending in `/`, and fails for a
|
|
407
|
+
// plain file or a missing path — so the trailing slash the canonical
|
|
408
|
+
// root form already carries is what makes this a directory test.
|
|
409
|
+
async function isReadableDir(rootUrl) {
|
|
410
|
+
try {
|
|
411
|
+
await fetchFileUrlText(rootUrl);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function initGithubTokenControls() {
|
|
419
|
+
const input = document.getElementById('github-token');
|
|
420
|
+
const revealBtn = document.getElementById('github-token-reveal');
|
|
421
|
+
const saveBtn = document.getElementById('github-token-save');
|
|
422
|
+
const clearBtn = document.getElementById('github-token-clear');
|
|
423
|
+
const status = document.getElementById('github-token-status');
|
|
424
|
+
if (!input || !revealBtn || !saveBtn || !clearBtn || !status)
|
|
425
|
+
return;
|
|
426
|
+
// Hydrate from storage. We deliberately do NOT pre-fill the input
|
|
427
|
+
// when a token already exists — instead show a "token is set" status
|
|
428
|
+
// and leave the field empty, so a shoulder-surfer (or screenshot)
|
|
429
|
+
// doesn't reveal the value. Saving an empty input is a no-op; Clear
|
|
430
|
+
// is the only way to remove a stored token.
|
|
431
|
+
let storedToken = '';
|
|
432
|
+
void chrome.storage.local.get(GITHUB_AUTH_TOKEN_KEY).then((got) => {
|
|
433
|
+
const raw = got[GITHUB_AUTH_TOKEN_KEY];
|
|
434
|
+
storedToken = typeof raw === 'string' ? raw : '';
|
|
435
|
+
renderStatus();
|
|
436
|
+
});
|
|
437
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
438
|
+
if (areaName !== 'local')
|
|
439
|
+
return;
|
|
440
|
+
const change = changes[GITHUB_AUTH_TOKEN_KEY];
|
|
441
|
+
if (!change)
|
|
442
|
+
return;
|
|
443
|
+
storedToken = typeof change.newValue === 'string' ? change.newValue : '';
|
|
444
|
+
renderStatus();
|
|
445
|
+
});
|
|
446
|
+
revealBtn.addEventListener('click', () => {
|
|
447
|
+
const visible = input.type === 'text';
|
|
448
|
+
input.type = visible ? 'password' : 'text';
|
|
449
|
+
revealBtn.textContent = visible ? 'Show' : 'Hide';
|
|
450
|
+
});
|
|
451
|
+
saveBtn.addEventListener('click', () => {
|
|
452
|
+
const next = input.value.trim();
|
|
453
|
+
if (!next) {
|
|
454
|
+
setStatus('Enter a token, or use Clear to remove the saved one.', 'err');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
void writeSetting(GITHUB_AUTH_TOKEN_KEY, next).then(() => {
|
|
458
|
+
input.value = '';
|
|
459
|
+
input.type = 'password';
|
|
460
|
+
revealBtn.textContent = 'Show';
|
|
461
|
+
// Reuse the same "A token is saved (ends in …XXXX)" message
|
|
462
|
+
// renderStatus produces on a hydrated page. Don't show a transient
|
|
463
|
+
// "Token saved." line that vanishes — the persistent message
|
|
464
|
+
// makes it obvious there IS a token, and which one.
|
|
465
|
+
// (The storage.onChanged listener will also fire renderStatus,
|
|
466
|
+
// but the explicit call here keeps the UI immediate.)
|
|
467
|
+
storedToken = next;
|
|
468
|
+
renderStatus();
|
|
469
|
+
}, (err) => setStatus(`Could not save the token: ${describeError(err)}`, 'err'));
|
|
470
|
+
});
|
|
471
|
+
clearBtn.addEventListener('click', () => {
|
|
472
|
+
void writeSetting(GITHUB_AUTH_TOKEN_KEY, null).then(() => {
|
|
473
|
+
input.value = '';
|
|
474
|
+
// Mirror Save's input reset so Show→Clear doesn't leave the
|
|
475
|
+
// (now empty) field stuck in text mode with a "Hide" button.
|
|
476
|
+
input.type = 'password';
|
|
477
|
+
revealBtn.textContent = 'Show';
|
|
478
|
+
storedToken = '';
|
|
479
|
+
renderStatus();
|
|
480
|
+
}, (err) => setStatus(`Could not remove the token: ${describeError(err)}`, 'err'));
|
|
481
|
+
});
|
|
482
|
+
// Allow Enter inside the input to save, so users don't have to mouse
|
|
483
|
+
// over to the Save button.
|
|
484
|
+
input.addEventListener('keydown', (ev) => {
|
|
485
|
+
if (ev.key === 'Enter') {
|
|
486
|
+
ev.preventDefault();
|
|
487
|
+
saveBtn.click();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
function renderStatus() {
|
|
491
|
+
if (storedToken) {
|
|
492
|
+
setStatus(`A token is saved (ends in …${storedToken.slice(-4)}). Saved tokens are never re-displayed; paste a new one to replace it.`, 'ok');
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
setStatus('No token saved. SourceMonkey will use unauthenticated GitHub API calls (60/hr).', '');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function setStatus(text, cls) {
|
|
499
|
+
status.textContent = text;
|
|
500
|
+
status.classList.remove('ok', 'err');
|
|
501
|
+
if (cls)
|
|
502
|
+
status.classList.add(cls);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// The viewer's heading: the shared vocabulary for it, the DOM that
|
|
2
|
+
// paints its leading terms, and the early paint that puts a name in the
|
|
3
|
+
// header bar before the rest of the page arrives.
|
|
4
|
+
//
|
|
5
|
+
// Why this is its own module, loaded from `userscript.html` ahead of
|
|
6
|
+
// `userscript.js`:
|
|
7
|
+
// - `userscript.js` is a large module graph that used to sit behind
|
|
8
|
+
// blocking Prism scripts. The browser's first paint landed well
|
|
9
|
+
// before any of it evaluated, so the header bar drew with an icon
|
|
10
|
+
// and nothing else and filled in a beat later — the one thing on
|
|
11
|
+
// the page that is supposed to say what the page IS.
|
|
12
|
+
// - This module has NO static imports, which is the point: a module
|
|
13
|
+
// does not evaluate until everything it imports has been fetched,
|
|
14
|
+
// so even one small import would put its first line behind another
|
|
15
|
+
// round trip and back after the first paint. Everything the early
|
|
16
|
+
// paint needs beyond the prefix is pulled in with a dynamic
|
|
17
|
+
// `import()`, after the prefix is already on screen.
|
|
18
|
+
// - `userscript.ts` imports it as well. A module runs once, so the
|
|
19
|
+
// early paint happens exactly once and the constants below have a
|
|
20
|
+
// single home rather than a copy per entry point.
|
|
21
|
+
//
|
|
22
|
+
// `import type` is fine here — it is erased, so it costs no fetch.
|
|
23
|
+
// What every heading's leading terms are joined by — the same `|` the
|
|
24
|
+
// tab titles use (architecture.md, "Page names").
|
|
25
|
+
export const HEADING_SEPARATOR = ' | ';
|
|
26
|
+
// The product run, which every heading opens with and which the header
|
|
27
|
+
// bar paints in the brand colour.
|
|
28
|
+
const BRAND_PREFIX = `SourceMonkey${HEADING_SEPARATOR}`;
|
|
29
|
+
export const INSTALLED_HEADING = BRAND_PREFIX;
|
|
30
|
+
// The verb is a page name beside the product's, not a word governing
|
|
31
|
+
// the script's — so it takes its own separator rather than running
|
|
32
|
+
// straight into the favicon and the name.
|
|
33
|
+
export const PREVIEW_HEADING = `${BRAND_PREFIX}Install${HEADING_SEPARATOR}`;
|
|
34
|
+
// The installed title takes no prefix — a tab strip has little room,
|
|
35
|
+
// and the script's own name is what the reader is scanning for. The
|
|
36
|
+
// preview spends that room: "which of my tabs is asking to install
|
|
37
|
+
// something" is the question a preview tab has to answer.
|
|
38
|
+
export const INSTALLED_TITLE = '';
|
|
39
|
+
export const PREVIEW_TITLE = 'Install script: ';
|
|
40
|
+
// Stands in for the label when there is no script to name.
|
|
41
|
+
export const MISSING_LABEL = 'Not available';
|
|
42
|
+
// Same spelling as script-store's own (which is module-private), and as
|
|
43
|
+
// the copies in background.ts / injector.ts / script-log-id.ts. Reading
|
|
44
|
+
// one row is all this module does; importing the store to get the key
|
|
45
|
+
// would pull its whole graph in and undo the point of being early.
|
|
46
|
+
const SCRIPT_PREFIX = 'script:';
|
|
47
|
+
// The heading's leading run of product text. Shared with `showMissing`,
|
|
48
|
+
// which draws a heading with no script in it — the class, the constant
|
|
49
|
+
// and the ordering must not be written twice and drift.
|
|
50
|
+
//
|
|
51
|
+
// The product name and every separator wear `.brand` and take the
|
|
52
|
+
// header bar's colour (src/styles.css); the page names between them
|
|
53
|
+
// read in the heading's own. So the preview's heading is a brown
|
|
54
|
+
// "SourceMonkey | … | " skeleton with a dark "Install" set into it,
|
|
55
|
+
// which is the same shape the other pages' headings have — theirs just
|
|
56
|
+
// have no middle term.
|
|
57
|
+
export function headingPrefixEl(prefix) {
|
|
58
|
+
const frag = document.createDocumentFragment();
|
|
59
|
+
// A prefix always ends in a separator, so the last part is empty and
|
|
60
|
+
// contributes nothing but its own `|`.
|
|
61
|
+
const [product, ...rest] = prefix.split(HEADING_SEPARATOR);
|
|
62
|
+
frag.appendChild(brandHomeLink(product));
|
|
63
|
+
for (const part of rest) {
|
|
64
|
+
frag.appendChild(prefixSpan(HEADING_SEPARATOR, true));
|
|
65
|
+
if (part)
|
|
66
|
+
frag.appendChild(prefixSpan(part, false));
|
|
67
|
+
}
|
|
68
|
+
return frag;
|
|
69
|
+
}
|
|
70
|
+
// The product name, as a link home to the collections page — the same
|
|
71
|
+
// element `userscript.html` ships in its markup, so repainting the
|
|
72
|
+
// heading doesn't change what the bar's first term is.
|
|
73
|
+
//
|
|
74
|
+
// A plain anchor with a real href, and no listener of its own: the
|
|
75
|
+
// click is caught by the page's delegated handler
|
|
76
|
+
// (`installHomeLinkHandler`, src/extension/page-links.ts), which is what makes it
|
|
77
|
+
// open the NAMED collections tab. Importing that here would put this
|
|
78
|
+
// module's first line behind another fetch and undo the early paint, so
|
|
79
|
+
// the href + `target` are also the behaviour before it runs.
|
|
80
|
+
function brandHomeLink(text) {
|
|
81
|
+
const el = document.createElement('a');
|
|
82
|
+
el.className = 'heading-prefix brand page-home';
|
|
83
|
+
el.href = 'collections.html';
|
|
84
|
+
el.target = '_blank';
|
|
85
|
+
el.rel = 'noopener';
|
|
86
|
+
el.title = 'Open the collections page';
|
|
87
|
+
el.textContent = text;
|
|
88
|
+
return el;
|
|
89
|
+
}
|
|
90
|
+
function prefixSpan(text, brand) {
|
|
91
|
+
const el = document.createElement('span');
|
|
92
|
+
el.className = brand ? 'heading-prefix brand' : 'heading-prefix';
|
|
93
|
+
el.textContent = text;
|
|
94
|
+
return el;
|
|
95
|
+
}
|
|
96
|
+
// `<label> | SourceMonkey`, with the preview's own prefix in front so a
|
|
97
|
+
// tab strip full of viewer tabs says which is an install question and
|
|
98
|
+
// which is a script already installed.
|
|
99
|
+
export function setPageTitle(prefix, label) {
|
|
100
|
+
document.title = `${prefix}${label} | SourceMonkey`;
|
|
101
|
+
}
|
|
102
|
+
// The favicon's fixed-width slot, empty. `renderHeading` fills it with
|
|
103
|
+
// the real thing later; drawing it now is what keeps the name from
|
|
104
|
+
// shifting sideways when that lands.
|
|
105
|
+
function emptyIconSlot() {
|
|
106
|
+
const el = document.createElement('span');
|
|
107
|
+
el.className = 'site-icon';
|
|
108
|
+
return el;
|
|
109
|
+
}
|
|
110
|
+
// Whether the full page has drawn this heading yet. The early paint is
|
|
111
|
+
// a placeholder for exactly as long as that hasn't happened, and it must
|
|
112
|
+
// never land on top of the real thing — `userscript.ts` calls
|
|
113
|
+
// `notePainted` from every path that writes the heading, and its storage
|
|
114
|
+
// reads can finish either side of ours.
|
|
115
|
+
//
|
|
116
|
+
// A flag rather than an inspection of the heading's own contents: the
|
|
117
|
+
// markup's brand run and a finished heading are both just spans and
|
|
118
|
+
// text, so any test on the DOM comes down to matching bytes that the
|
|
119
|
+
// markup is free to change without failing anything.
|
|
120
|
+
let painted = false;
|
|
121
|
+
export function notePainted() {
|
|
122
|
+
painted = true;
|
|
123
|
+
}
|
|
124
|
+
// ---------- The early paint ----------
|
|
125
|
+
// Everything above is also used by `userscript.ts`; the rest of this
|
|
126
|
+
// module is the side effect that makes loading it early worth doing.
|
|
127
|
+
//
|
|
128
|
+
// The prefix goes on synchronously, before any `await` or `import()`,
|
|
129
|
+
// because it is the half we can know without reading anything: which of
|
|
130
|
+
// the two pages this is. Only the script's name needs a storage read.
|
|
131
|
+
const headingEl = document.getElementById('filename');
|
|
132
|
+
const searchParams = new URLSearchParams(location.search);
|
|
133
|
+
const earlyScriptId = searchParams.get('script');
|
|
134
|
+
if (headingEl) {
|
|
135
|
+
// The install page's own name for itself. The installed page's prefix
|
|
136
|
+
// is already in the markup, but it is repainted here anyway so both
|
|
137
|
+
// modes leave the heading in the same shape — prefix, then the
|
|
138
|
+
// favicon's fixed-width slot, so the name doesn't shift sideways when
|
|
139
|
+
// the real favicon lands.
|
|
140
|
+
const prefix = earlyScriptId ? INSTALLED_HEADING : PREVIEW_HEADING;
|
|
141
|
+
if (earlyScriptId || searchParams.get('preview')) {
|
|
142
|
+
headingEl.replaceChildren(headingPrefixEl(prefix), emptyIconSlot());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
void paintEarlyName();
|
|
146
|
+
// The script's name, which needs the one `script:<id>` row plus the
|
|
147
|
+
// helpers that turn it into a label. A preview has no name to paint:
|
|
148
|
+
// its script's `@name` is inside a file that hasn't been fetched, and
|
|
149
|
+
// naming it from the URL would only move the flash — the location it
|
|
150
|
+
// would show is usually not what the finished heading says.
|
|
151
|
+
async function paintEarlyName() {
|
|
152
|
+
if (!headingEl || !earlyScriptId)
|
|
153
|
+
return;
|
|
154
|
+
const key = `${SCRIPT_PREFIX}${earlyScriptId}`;
|
|
155
|
+
const [got, display] = await Promise.all([
|
|
156
|
+
chrome.storage.local.get(key),
|
|
157
|
+
import('../script-display.js'),
|
|
158
|
+
]);
|
|
159
|
+
const script = got[key];
|
|
160
|
+
if (!script)
|
|
161
|
+
return;
|
|
162
|
+
// Bail if the full page beat us to it while we were reading.
|
|
163
|
+
if (painted)
|
|
164
|
+
return;
|
|
165
|
+
const label = display.scriptLabel(script.header, script.relativePath);
|
|
166
|
+
// The same slash-aware wrap `renderHeading` uses, so the finished
|
|
167
|
+
// heading breaks its lines exactly where this one did.
|
|
168
|
+
display.appendWithSlashBreaks(headingEl, label);
|
|
169
|
+
setPageTitle(INSTALLED_TITLE, label);
|
|
170
|
+
}
|