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,203 @@
|
|
|
1
|
+
import { renderGlyph, setGlyphButton, setGlyphButtonBusy, } from '../glyph.js';
|
|
2
|
+
import { formatLogTimestamp } from '../log-format.js';
|
|
3
|
+
import { installHomeLinkHandler } from '../page-links.js';
|
|
4
|
+
import { formatBytes } from '../../script-display.js';
|
|
5
|
+
import { isIconCacheKey, listCachedIcons, } from '../site-icon.js';
|
|
6
|
+
import { deleteAllCachedIcons, deleteCachedIcon, refreshCachedIcon } from '../worker-client.js';
|
|
7
|
+
const summaryEl = document.getElementById('icons-summary');
|
|
8
|
+
const emptyEl = document.getElementById('icons-empty');
|
|
9
|
+
const tableEl = document.getElementById('icons-table');
|
|
10
|
+
const tbodyEl = document.getElementById('icons-tbody');
|
|
11
|
+
const deleteAllBtn = document.getElementById('delete-all');
|
|
12
|
+
const REFRESH_LABELS = {
|
|
13
|
+
idle: 'Refresh cached icon',
|
|
14
|
+
busy: 'Refreshing…',
|
|
15
|
+
};
|
|
16
|
+
const refreshingKeys = new Set();
|
|
17
|
+
let renderGeneration = 0;
|
|
18
|
+
// The log's local-time spelling, without its milliseconds: cache
|
|
19
|
+
// timestamps are days apart, so the tail is noise here.
|
|
20
|
+
function formatTimestamp(ms) {
|
|
21
|
+
if (Number.isNaN(new Date(ms).getTime()))
|
|
22
|
+
return '—';
|
|
23
|
+
return formatLogTimestamp(ms).replace(/\.\d{3}$/, '');
|
|
24
|
+
}
|
|
25
|
+
// Storage failures on this page have no surface of their own; an
|
|
26
|
+
// unhandled rejection would land in chrome://extensions/?errors, where
|
|
27
|
+
// it reads as SourceMonkey being broken.
|
|
28
|
+
function report(what) {
|
|
29
|
+
return (err) => console.info(`[SourceMonkey] debug icons: ${what} failed`, err);
|
|
30
|
+
}
|
|
31
|
+
function formatIso(ms) {
|
|
32
|
+
const d = new Date(ms);
|
|
33
|
+
return Number.isNaN(d.getTime()) ? '' : d.toISOString();
|
|
34
|
+
}
|
|
35
|
+
function kindLabel(kind) {
|
|
36
|
+
return kind === 'url' ? '@icon' : kind;
|
|
37
|
+
}
|
|
38
|
+
function renderRow(row) {
|
|
39
|
+
const tr = document.createElement('tr');
|
|
40
|
+
tr.dataset.storageKey = row.storageKey;
|
|
41
|
+
// 1. Icon
|
|
42
|
+
const iconTd = document.createElement('td');
|
|
43
|
+
iconTd.className = 'col-icon';
|
|
44
|
+
if (row.entry.dataUrl) {
|
|
45
|
+
const img = document.createElement('img');
|
|
46
|
+
img.src = row.entry.dataUrl;
|
|
47
|
+
img.alt = '';
|
|
48
|
+
iconTd.appendChild(img);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
iconTd.appendChild(renderGlyph(document, '#sm-globe', 'site-icon-globe'));
|
|
52
|
+
}
|
|
53
|
+
tr.appendChild(iconTd);
|
|
54
|
+
// 2. Site / Target
|
|
55
|
+
const targetTd = document.createElement('td');
|
|
56
|
+
targetTd.className = 'col-target';
|
|
57
|
+
const link = document.createElement('a');
|
|
58
|
+
link.href = row.target;
|
|
59
|
+
link.target = '_blank';
|
|
60
|
+
link.rel = 'noopener noreferrer';
|
|
61
|
+
link.textContent = row.target;
|
|
62
|
+
link.title = row.storageKey;
|
|
63
|
+
targetTd.appendChild(link);
|
|
64
|
+
tr.appendChild(targetTd);
|
|
65
|
+
// 3. Scope
|
|
66
|
+
const kindTd = document.createElement('td');
|
|
67
|
+
const chip = document.createElement('span');
|
|
68
|
+
chip.className = `kind-chip kind-${row.kind}`;
|
|
69
|
+
chip.textContent = kindLabel(row.kind);
|
|
70
|
+
chip.title = row.storageKey;
|
|
71
|
+
kindTd.appendChild(chip);
|
|
72
|
+
tr.appendChild(kindTd);
|
|
73
|
+
// 4. Details
|
|
74
|
+
const detailsTd = document.createElement('td');
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (row.entry.dataUrl) {
|
|
77
|
+
const mime = row.mimeType ?? 'image';
|
|
78
|
+
const size = row.byteLength !== null ? formatBytes(row.byteLength) : '';
|
|
79
|
+
parts.push(size ? `${mime} (${size})` : mime);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
parts.push('none (miss)');
|
|
83
|
+
}
|
|
84
|
+
if (row.entry.v !== undefined) {
|
|
85
|
+
parts.push(`v${row.entry.v}`);
|
|
86
|
+
}
|
|
87
|
+
detailsTd.textContent = parts.join(' · ');
|
|
88
|
+
const badge = document.createElement('span');
|
|
89
|
+
if (!row.isFresh) {
|
|
90
|
+
badge.className = 'status-badge status-stale';
|
|
91
|
+
badge.textContent = 'expired';
|
|
92
|
+
}
|
|
93
|
+
else if (row.requiredVersion !== null && row.entry.v !== row.requiredVersion) {
|
|
94
|
+
badge.className = 'status-badge status-stale';
|
|
95
|
+
badge.textContent = 'stale schema';
|
|
96
|
+
const have = row.entry.v === undefined ? 'none' : `v${row.entry.v}`;
|
|
97
|
+
badge.title = [
|
|
98
|
+
`Schema version: ${have}`,
|
|
99
|
+
`Current version: v${row.requiredVersion}`,
|
|
100
|
+
'Ignored and refetched on the next render.',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
else if (!row.isUsable) {
|
|
104
|
+
badge.className = 'status-badge status-stale';
|
|
105
|
+
badge.textContent = 'unpaintable';
|
|
106
|
+
badge.title = [
|
|
107
|
+
'The cached value is not an icon we would paint.',
|
|
108
|
+
'Ignored and refetched on the next render.',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
else if (row.entry.dataUrl === null) {
|
|
112
|
+
badge.className = 'status-badge status-miss';
|
|
113
|
+
badge.textContent = 'miss';
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
badge.className = 'status-badge status-ok';
|
|
117
|
+
badge.textContent = 'ok';
|
|
118
|
+
}
|
|
119
|
+
detailsTd.appendChild(badge);
|
|
120
|
+
tr.appendChild(detailsTd);
|
|
121
|
+
// 5. Fetched timestamp
|
|
122
|
+
const fetchedTd = document.createElement('td');
|
|
123
|
+
fetchedTd.className = 'col-time';
|
|
124
|
+
fetchedTd.textContent = formatTimestamp(row.entry.fetchedAt);
|
|
125
|
+
fetchedTd.title = formatIso(row.entry.fetchedAt);
|
|
126
|
+
tr.appendChild(fetchedTd);
|
|
127
|
+
// 6. Expiry timestamp
|
|
128
|
+
const expiresTd = document.createElement('td');
|
|
129
|
+
expiresTd.className = 'col-time';
|
|
130
|
+
expiresTd.textContent = formatTimestamp(row.expiresAt);
|
|
131
|
+
expiresTd.title = formatIso(row.expiresAt);
|
|
132
|
+
tr.appendChild(expiresTd);
|
|
133
|
+
// 7. Actions (Refresh + Delete)
|
|
134
|
+
const actionsTd = document.createElement('td');
|
|
135
|
+
actionsTd.className = 'col-actions';
|
|
136
|
+
const wrap = document.createElement('div');
|
|
137
|
+
wrap.className = 'row-actions';
|
|
138
|
+
const refreshBtn = document.createElement('button');
|
|
139
|
+
refreshBtn.type = 'button';
|
|
140
|
+
refreshBtn.className = 'refresh-icon';
|
|
141
|
+
setGlyphButton(refreshBtn, '#sm-refresh', REFRESH_LABELS.idle);
|
|
142
|
+
if (refreshingKeys.has(row.storageKey)) {
|
|
143
|
+
setGlyphButtonBusy(refreshBtn, true, REFRESH_LABELS);
|
|
144
|
+
}
|
|
145
|
+
// The storage write at the end of a refresh re-renders the table via
|
|
146
|
+
// `onChanged`, so this button is replaced rather than un-busied; the
|
|
147
|
+
// explicit render covers the refresh that stored nothing new.
|
|
148
|
+
refreshBtn.addEventListener('click', () => {
|
|
149
|
+
refreshingKeys.add(row.storageKey);
|
|
150
|
+
setGlyphButtonBusy(refreshBtn, true, REFRESH_LABELS);
|
|
151
|
+
void refreshCachedIcon(row.storageKey)
|
|
152
|
+
.catch(report('refresh'))
|
|
153
|
+
.finally(() => {
|
|
154
|
+
refreshingKeys.delete(row.storageKey);
|
|
155
|
+
void render();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
const deleteBtn = document.createElement('button');
|
|
159
|
+
deleteBtn.type = 'button';
|
|
160
|
+
deleteBtn.className = 'delete-icon';
|
|
161
|
+
setGlyphButton(deleteBtn, '#sm-trash', 'Delete cached icon');
|
|
162
|
+
deleteBtn.addEventListener('click', () => {
|
|
163
|
+
deleteBtn.disabled = true;
|
|
164
|
+
void deleteCachedIcon(row.storageKey)
|
|
165
|
+
.catch(report('delete'))
|
|
166
|
+
.finally(() => {
|
|
167
|
+
void render();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
wrap.append(refreshBtn, deleteBtn);
|
|
171
|
+
actionsTd.appendChild(wrap);
|
|
172
|
+
tr.appendChild(actionsTd);
|
|
173
|
+
return tr;
|
|
174
|
+
}
|
|
175
|
+
async function render() {
|
|
176
|
+
const gen = ++renderGeneration;
|
|
177
|
+
const rows = await listCachedIcons();
|
|
178
|
+
if (gen !== renderGeneration)
|
|
179
|
+
return;
|
|
180
|
+
const count = rows.length;
|
|
181
|
+
summaryEl.textContent = count === 1 ? '1 cached icon' : `${count} cached icons`;
|
|
182
|
+
deleteAllBtn.disabled = count === 0;
|
|
183
|
+
emptyEl.hidden = count > 0;
|
|
184
|
+
tableEl.hidden = count === 0;
|
|
185
|
+
tbodyEl.replaceChildren(...rows.map(renderRow));
|
|
186
|
+
}
|
|
187
|
+
installHomeLinkHandler();
|
|
188
|
+
deleteAllBtn.addEventListener('click', () => {
|
|
189
|
+
deleteAllBtn.disabled = true;
|
|
190
|
+
void deleteAllCachedIcons()
|
|
191
|
+
.catch(report('delete all'))
|
|
192
|
+
.finally(() => {
|
|
193
|
+
void render();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
197
|
+
if (areaName !== 'local')
|
|
198
|
+
return;
|
|
199
|
+
if (Object.keys(changes).some(isIconCacheKey)) {
|
|
200
|
+
void render();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
void render();
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// The storage debug page (/debug/storage.html): where
|
|
2
|
+
// `chrome.storage.local` goes, by script, by stored file, or in GM
|
|
3
|
+
// values. Read-only. See docs/storage.md#storage-debug-page.
|
|
4
|
+
import { collectionAnchorId } from '../collection-display.js';
|
|
5
|
+
import { installHomeLinkHandler, namedPageUrl, wireNamedTabLink } from '../page-links.js';
|
|
6
|
+
import { timeStorageRead } from '../storage-scan.js';
|
|
7
|
+
import { describeError } from '../script-store.js';
|
|
8
|
+
import { appendWithSlashBreaks, viewerUrl } from '../../script-display.js';
|
|
9
|
+
import { buildStorageBreakdown, defaultSortDir, fileIdentity, sortFiles, sortScripts, sortValues, } from '../storage-breakdown.js';
|
|
10
|
+
const summaryEl = document.getElementById('storage-summary');
|
|
11
|
+
const emptyEl = document.getElementById('storage-empty');
|
|
12
|
+
const tableEl = document.getElementById('storage-table');
|
|
13
|
+
const viewButtons = [...document.querySelectorAll('.views button')];
|
|
14
|
+
installHomeLinkHandler();
|
|
15
|
+
// Each view keeps its own sort while the page is open.
|
|
16
|
+
const sorts = {
|
|
17
|
+
script: { key: 'size', dir: 'desc' },
|
|
18
|
+
file: { key: 'size', dir: 'desc' },
|
|
19
|
+
values: { key: 'size', dir: 'desc' },
|
|
20
|
+
};
|
|
21
|
+
let breakdown = null;
|
|
22
|
+
let loadError = '';
|
|
23
|
+
// The view is the URL's fragment, so a reload keeps it.
|
|
24
|
+
function currentView() {
|
|
25
|
+
const hash = location.hash.slice(1);
|
|
26
|
+
return hash === 'file' || hash === 'values' ? hash : 'script';
|
|
27
|
+
}
|
|
28
|
+
for (const button of viewButtons) {
|
|
29
|
+
button.addEventListener('click', () => {
|
|
30
|
+
const view = button.dataset.view;
|
|
31
|
+
history.replaceState(null, '', view === 'script' ? location.pathname : `#${view}`);
|
|
32
|
+
render();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
addEventListener('hashchange', render);
|
|
36
|
+
// ---------- Formatting ----------
|
|
37
|
+
const numberFormat = new Intl.NumberFormat();
|
|
38
|
+
function plural(n, one, many = `${one}s`) {
|
|
39
|
+
return `${numberFormat.format(n)} ${n === 1 ? one : many}`;
|
|
40
|
+
}
|
|
41
|
+
// Whole KB, as on the Options page; a size too small to show as 1 KB
|
|
42
|
+
// still reads as something rather than 0.
|
|
43
|
+
function formatKb(bytes) {
|
|
44
|
+
const kb = Math.round(bytes / 1024);
|
|
45
|
+
if (kb === 0 && bytes > 0)
|
|
46
|
+
return '< 1 KB';
|
|
47
|
+
return `${numberFormat.format(kb)} KB`;
|
|
48
|
+
}
|
|
49
|
+
const KIND_LABELS = {
|
|
50
|
+
script: 'script',
|
|
51
|
+
metadata: 'metadata',
|
|
52
|
+
require: '@require',
|
|
53
|
+
resource: '@resource',
|
|
54
|
+
'smdev-text': 'sm-dev script text',
|
|
55
|
+
'smdev-file': 'sm-dev pushed file',
|
|
56
|
+
};
|
|
57
|
+
// ---------- Cells ----------
|
|
58
|
+
function span(className, text) {
|
|
59
|
+
const el = document.createElement('span');
|
|
60
|
+
el.className = className;
|
|
61
|
+
el.textContent = text;
|
|
62
|
+
return el;
|
|
63
|
+
}
|
|
64
|
+
function td(className, ...children) {
|
|
65
|
+
const cell = document.createElement('td');
|
|
66
|
+
if (className)
|
|
67
|
+
cell.className = className;
|
|
68
|
+
// Spaces between the parts, so a kind, a name and a location wrap
|
|
69
|
+
// as words.
|
|
70
|
+
children.forEach((child, i) => {
|
|
71
|
+
if (i > 0)
|
|
72
|
+
cell.append(' ');
|
|
73
|
+
cell.append(child);
|
|
74
|
+
});
|
|
75
|
+
return cell;
|
|
76
|
+
}
|
|
77
|
+
// A name cell running down beside the group's item lines, so a long
|
|
78
|
+
// name wraps alongside them rather than pushing them down.
|
|
79
|
+
function spanGroup(cell, items) {
|
|
80
|
+
cell.rowSpan = 1 + items;
|
|
81
|
+
return cell;
|
|
82
|
+
}
|
|
83
|
+
function sizeCell(bytes) {
|
|
84
|
+
const cell = td('num', formatKb(bytes));
|
|
85
|
+
cell.title = `${numberFormat.format(bytes)} bytes`;
|
|
86
|
+
return cell;
|
|
87
|
+
}
|
|
88
|
+
// Blank for a file only one script uses.
|
|
89
|
+
function sharedCell(shared) {
|
|
90
|
+
return td('num', shared > 1 ? numberFormat.format(shared) : '');
|
|
91
|
+
}
|
|
92
|
+
function collectionCell(ref) {
|
|
93
|
+
if (!ref.collectionId)
|
|
94
|
+
return td(undefined, span('muted', 'No collection'));
|
|
95
|
+
const a = document.createElement('a');
|
|
96
|
+
// Break after slashes, as the collections page does: a name is often a path.
|
|
97
|
+
appendWithSlashBreaks(a, ref.collectionName);
|
|
98
|
+
const hash = collectionAnchorId(ref.collectionId);
|
|
99
|
+
wireNamedTabLink(a, 'collections', hash);
|
|
100
|
+
// Its page-relative href would resolve inside debug/, beside this page.
|
|
101
|
+
a.href = chrome.runtime.getURL(namedPageUrl('collections', hash));
|
|
102
|
+
return td(undefined, a);
|
|
103
|
+
}
|
|
104
|
+
function scriptCell(ref) {
|
|
105
|
+
const a = document.createElement('a');
|
|
106
|
+
appendWithSlashBreaks(a, ref.scriptName);
|
|
107
|
+
a.href = `../${viewerUrl(ref.scriptId)}`;
|
|
108
|
+
a.target = '_blank';
|
|
109
|
+
a.rel = 'noopener';
|
|
110
|
+
return td(undefined, a);
|
|
111
|
+
}
|
|
112
|
+
// The parts naming an item: its kind, `@resource` name and location,
|
|
113
|
+
// whichever it has and the caller wants.
|
|
114
|
+
function itemParts(item) {
|
|
115
|
+
const parts = [];
|
|
116
|
+
if (item.kind)
|
|
117
|
+
parts.push(span('kind', KIND_LABELS[item.kind]));
|
|
118
|
+
if (item.name)
|
|
119
|
+
parts.push(span('loc', item.name));
|
|
120
|
+
if (item.location) {
|
|
121
|
+
const loc = span('loc', '');
|
|
122
|
+
appendWithSlashBreaks(loc, item.location);
|
|
123
|
+
parts.push(loc);
|
|
124
|
+
}
|
|
125
|
+
if (item.missing)
|
|
126
|
+
parts.push(span('muted', MISSING));
|
|
127
|
+
return parts;
|
|
128
|
+
}
|
|
129
|
+
// A file scripts point at that isn't in storage (see `missing` in
|
|
130
|
+
// storage-breakdown.ts).
|
|
131
|
+
const MISSING = '(missing from storage)';
|
|
132
|
+
function row(className, cells) {
|
|
133
|
+
const tr = document.createElement('tr');
|
|
134
|
+
tr.className = className;
|
|
135
|
+
tr.append(...cells);
|
|
136
|
+
return tr;
|
|
137
|
+
}
|
|
138
|
+
function group(rows) {
|
|
139
|
+
const tbody = document.createElement('tbody');
|
|
140
|
+
tbody.className = 'group';
|
|
141
|
+
tbody.append(...rows);
|
|
142
|
+
return tbody;
|
|
143
|
+
}
|
|
144
|
+
function head(view, columns) {
|
|
145
|
+
const colgroup = document.createElement('colgroup');
|
|
146
|
+
for (const c of columns) {
|
|
147
|
+
const col = document.createElement('col');
|
|
148
|
+
if (c.width)
|
|
149
|
+
col.className = c.width;
|
|
150
|
+
colgroup.append(col);
|
|
151
|
+
}
|
|
152
|
+
const thead = document.createElement('thead');
|
|
153
|
+
const tr = document.createElement('tr');
|
|
154
|
+
const sort = sorts[view];
|
|
155
|
+
for (const c of columns) {
|
|
156
|
+
const th = document.createElement('th');
|
|
157
|
+
th.textContent = c.label;
|
|
158
|
+
const classes = [c.num ? 'num' : '', c.sort ? 'sortable' : ''].filter(Boolean);
|
|
159
|
+
if (classes.length)
|
|
160
|
+
th.className = classes.join(' ');
|
|
161
|
+
if (c.sort) {
|
|
162
|
+
const key = c.sort;
|
|
163
|
+
th.dataset.sort = key;
|
|
164
|
+
if (sort.key === key) {
|
|
165
|
+
th.append(span('sort-arrow', sort.dir === 'desc' ? '▼' : '▲'));
|
|
166
|
+
th.setAttribute('aria-sort', sort.dir === 'desc' ? 'descending' : 'ascending');
|
|
167
|
+
}
|
|
168
|
+
th.addEventListener('click', () => {
|
|
169
|
+
sorts[view] = sort.key === key
|
|
170
|
+
? { key, dir: sort.dir === 'desc' ? 'asc' : 'desc' }
|
|
171
|
+
: { key, dir: defaultSortDir(key) };
|
|
172
|
+
render();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
tr.append(th);
|
|
176
|
+
}
|
|
177
|
+
thead.append(tr);
|
|
178
|
+
return [colgroup, thead];
|
|
179
|
+
}
|
|
180
|
+
function scriptTable(b) {
|
|
181
|
+
const { key, dir } = sorts.script;
|
|
182
|
+
return [
|
|
183
|
+
...head('script', [
|
|
184
|
+
{ label: 'Collection', sort: 'collection', width: 'col-collection' },
|
|
185
|
+
{ label: 'Script', sort: 'script', width: 'col-name' },
|
|
186
|
+
{ label: 'File' },
|
|
187
|
+
{ label: 'Size', sort: 'size', width: 'col-size', num: true },
|
|
188
|
+
{ label: 'Shared copies', sort: 'shared', width: 'col-shared', num: true },
|
|
189
|
+
]),
|
|
190
|
+
...sortScripts(b.scripts, key, dir).map((s) => group([
|
|
191
|
+
row('group-head', [
|
|
192
|
+
spanGroup(collectionCell(s), s.items.length), spanGroup(scriptCell(s), s.items.length),
|
|
193
|
+
td('total-label', 'Total'), sizeCell(s.bytes), td(),
|
|
194
|
+
]),
|
|
195
|
+
...s.items.map((item) => row('group-item', [
|
|
196
|
+
td('item', ...itemParts(item)), sizeCell(item.bytes), sharedCell(item.shared),
|
|
197
|
+
])),
|
|
198
|
+
])),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
function fileGroup(file) {
|
|
202
|
+
const id = fileIdentity(file);
|
|
203
|
+
// The heading names what every use agrees on; each use's line names
|
|
204
|
+
// the rest.
|
|
205
|
+
const heading = id.kind === null
|
|
206
|
+
? [span('muted', 'Multiple locations')]
|
|
207
|
+
: [
|
|
208
|
+
...itemParts({
|
|
209
|
+
kind: id.kind,
|
|
210
|
+
name: id.sameName ? id.name : undefined,
|
|
211
|
+
location: id.sameLocation ? id.location : undefined,
|
|
212
|
+
}),
|
|
213
|
+
...(id.sameLocation ? [] : [span('muted', 'multiple locations')]),
|
|
214
|
+
];
|
|
215
|
+
if (file.missing)
|
|
216
|
+
heading.push(span('muted', MISSING));
|
|
217
|
+
const own = (use) => id.kind === null ? itemParts(use) : itemParts({
|
|
218
|
+
name: id.sameName ? undefined : use.name,
|
|
219
|
+
location: id.sameLocation ? undefined : use.location,
|
|
220
|
+
});
|
|
221
|
+
const useCells = (use) => [collectionCell(use), scriptCell(use)].map((cell) => {
|
|
222
|
+
cell.classList.add('use');
|
|
223
|
+
return cell;
|
|
224
|
+
});
|
|
225
|
+
// When the heading says it all, the first use goes on the heading's
|
|
226
|
+
// line rather than leaving a gap above the uses, and the heading runs
|
|
227
|
+
// down beside the rest, so a long one wraps without pushing them down.
|
|
228
|
+
const [first, ...rest] = file.uses;
|
|
229
|
+
const lead = first && file.uses.every((use) => own(use).length === 0) ? first : undefined;
|
|
230
|
+
if (lead) {
|
|
231
|
+
return group([
|
|
232
|
+
row('group-head', [
|
|
233
|
+
spanGroup(td(undefined, ...heading), rest.length), ...useCells(lead),
|
|
234
|
+
sizeCell(file.bytes), sharedCell(file.uses.length),
|
|
235
|
+
]),
|
|
236
|
+
...rest.map((use) => row('group-item', [...useCells(use), td(), td()])),
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
return group([
|
|
240
|
+
row('group-head', [td(undefined, ...heading), td(), td(), sizeCell(file.bytes), sharedCell(file.uses.length)]),
|
|
241
|
+
...file.uses.map((use) => row('group-item', [
|
|
242
|
+
td('item', ...own(use)), ...useCells(use), td(), td(),
|
|
243
|
+
])),
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
function fileTable(b) {
|
|
247
|
+
const { key, dir } = sorts.file;
|
|
248
|
+
const parts = [
|
|
249
|
+
...head('file', [
|
|
250
|
+
{ label: 'File' },
|
|
251
|
+
{ label: 'Collection', width: 'col-collection' },
|
|
252
|
+
{ label: 'Script', width: 'col-name' },
|
|
253
|
+
{ label: 'Size', sort: 'size', width: 'col-size', num: true },
|
|
254
|
+
{ label: 'Shared copies', sort: 'shared', width: 'col-shared', num: true },
|
|
255
|
+
]),
|
|
256
|
+
// Only those two headings sort here.
|
|
257
|
+
...sortFiles(b.files, key === 'shared' ? 'shared' : 'size', dir).map(fileGroup),
|
|
258
|
+
];
|
|
259
|
+
// Always last: it belongs to no collection or script to sort by.
|
|
260
|
+
if (b.unused.length > 0) {
|
|
261
|
+
const unused = [...b.unused].sort((x, y) => y.bytes - x.bytes);
|
|
262
|
+
parts.push(group([
|
|
263
|
+
row('group-head', [
|
|
264
|
+
td(undefined, span('muted', 'Not used by any script')), td(), td(),
|
|
265
|
+
sizeCell(unused.reduce((sum, f) => sum + f.bytes, 0)), td(),
|
|
266
|
+
]),
|
|
267
|
+
...unused.map((f) => row('group-item', [
|
|
268
|
+
td('item', span('loc', f.key)), td(), td(), sizeCell(f.bytes), td(),
|
|
269
|
+
])),
|
|
270
|
+
]));
|
|
271
|
+
}
|
|
272
|
+
return parts;
|
|
273
|
+
}
|
|
274
|
+
function valuesTable(b) {
|
|
275
|
+
const { key, dir } = sorts.values;
|
|
276
|
+
return [
|
|
277
|
+
...head('values', [
|
|
278
|
+
{ label: 'Collection', sort: 'collection', width: 'col-values-name' },
|
|
279
|
+
{ label: 'Script', sort: 'script' },
|
|
280
|
+
{ label: 'Size', sort: 'size', width: 'col-size', num: true },
|
|
281
|
+
{ label: 'Values', sort: 'count', width: 'col-count', num: true },
|
|
282
|
+
]),
|
|
283
|
+
...sortValues(b.values, key, dir).map((v) => group([
|
|
284
|
+
row('group-head', v.script
|
|
285
|
+
? [collectionCell(v.script), scriptCell(v.script), sizeCell(v.bytes), td('num', numberFormat.format(v.count))]
|
|
286
|
+
: [
|
|
287
|
+
td(undefined, span('muted', 'Removed script')), td(undefined, span('loc', v.scriptId)),
|
|
288
|
+
sizeCell(v.bytes), td('num', numberFormat.format(v.count)),
|
|
289
|
+
]),
|
|
290
|
+
])),
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
// ---------- Render ----------
|
|
294
|
+
function summary(view, b) {
|
|
295
|
+
if (view === 'script') {
|
|
296
|
+
// Scripts whose collection row is missing ('') aren't one more.
|
|
297
|
+
const collections = new Set(b.scripts.map((s) => s.collectionId).filter(Boolean)).size;
|
|
298
|
+
return `${plural(b.scripts.length, 'script')} in ${plural(collections, 'collection')}. `
|
|
299
|
+
+ 'Shared files are counted once in each script that references them.';
|
|
300
|
+
}
|
|
301
|
+
if (view === 'file') {
|
|
302
|
+
// A missing file isn't stored.
|
|
303
|
+
const all = [...b.files.filter((f) => !f.missing), ...b.unused];
|
|
304
|
+
const bytes = all.reduce((sum, f) => sum + f.bytes, 0);
|
|
305
|
+
return `${plural(all.length, 'stored file')}, ${formatKb(bytes)}. Each shared file counts once.`;
|
|
306
|
+
}
|
|
307
|
+
const count = b.values.reduce((sum, v) => sum + v.count, 0);
|
|
308
|
+
const bytes = b.values.reduce((sum, v) => sum + v.bytes, 0);
|
|
309
|
+
return `${plural(count, 'value')} in ${plural(b.values.length, 'script')}, ${formatKb(bytes)}.`;
|
|
310
|
+
}
|
|
311
|
+
const EMPTY = {
|
|
312
|
+
script: 'No scripts in storage.',
|
|
313
|
+
file: 'No stored files.',
|
|
314
|
+
values: 'No scripts have stored values.',
|
|
315
|
+
};
|
|
316
|
+
function render() {
|
|
317
|
+
const view = currentView();
|
|
318
|
+
for (const button of viewButtons)
|
|
319
|
+
button.setAttribute('aria-pressed', String(button.dataset.view === view));
|
|
320
|
+
if (!breakdown) {
|
|
321
|
+
summaryEl.textContent = loadError;
|
|
322
|
+
emptyEl.hidden = true;
|
|
323
|
+
tableEl.hidden = true;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const b = breakdown;
|
|
327
|
+
const isEmpty = view === 'script' ? b.scripts.length === 0
|
|
328
|
+
: view === 'file' ? b.files.length + b.unused.length === 0
|
|
329
|
+
: b.values.length === 0;
|
|
330
|
+
summaryEl.textContent = isEmpty ? '' : summary(view, b);
|
|
331
|
+
emptyEl.textContent = EMPTY[view];
|
|
332
|
+
emptyEl.hidden = !isEmpty;
|
|
333
|
+
tableEl.hidden = isEmpty;
|
|
334
|
+
tableEl.replaceChildren(...(isEmpty ? [] : view === 'script' ? scriptTable(b) : view === 'file' ? fileTable(b) : valuesTable(b)));
|
|
335
|
+
}
|
|
336
|
+
// Each load reads every row, so a burst of writes (a refresh, a script
|
|
337
|
+
// spamming GM_setValue) is coalesced into one reload a second at most.
|
|
338
|
+
// Loads can overlap; only the newest one may draw.
|
|
339
|
+
let latest = 0;
|
|
340
|
+
let pending = false;
|
|
341
|
+
async function load() {
|
|
342
|
+
pending = false;
|
|
343
|
+
const mine = ++latest;
|
|
344
|
+
try {
|
|
345
|
+
const rows = await timeStorageRead('storage debug page', () => chrome.storage.local.get(null));
|
|
346
|
+
if (mine !== latest)
|
|
347
|
+
return;
|
|
348
|
+
breakdown = buildStorageBreakdown(rows);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (mine !== latest)
|
|
352
|
+
return;
|
|
353
|
+
breakdown = null;
|
|
354
|
+
loadError = `Could not read storage: ${describeError(err)}`;
|
|
355
|
+
}
|
|
356
|
+
render();
|
|
357
|
+
}
|
|
358
|
+
chrome.storage.onChanged.addListener((_changes, areaName) => {
|
|
359
|
+
if (areaName !== 'local' || pending)
|
|
360
|
+
return;
|
|
361
|
+
pending = true;
|
|
362
|
+
setTimeout(() => void load(), 1000);
|
|
363
|
+
});
|
|
364
|
+
render();
|
|
365
|
+
void load();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Lets external pages (anywhere on the web) "navigate into" the
|
|
2
|
+
// extension. Plain web pages can't link to chrome-extension:// URLs,
|
|
3
|
+
// so we publish a stub docs/redirect-page.md on GitHub, watch for
|
|
4
|
+
// navigations to it, and rewrite the tab to the requested extension
|
|
5
|
+
// page.
|
|
6
|
+
//
|
|
7
|
+
// Source URL:
|
|
8
|
+
// https://github.com/jshute96/SourceMonkey/blob/main/docs/redirect-page.md
|
|
9
|
+
// ?page=<allowed>&<other-params>
|
|
10
|
+
// Target URL:
|
|
11
|
+
// chrome-extension://<id>/<page>?<other-params>
|
|
12
|
+
//
|
|
13
|
+
// `page` is allowlisted. Anything else short-circuits and leaves the
|
|
14
|
+
// GitHub page alone — the doc itself explains how to install the
|
|
15
|
+
// extension, so a non-redirect outcome is the "no extension" fallback.
|
|
16
|
+
//
|
|
17
|
+
// chrome.runtime.getURL() builds the full chrome-extension:// URL at
|
|
18
|
+
// runtime, so this works whether the extension was loaded unpacked
|
|
19
|
+
// (ID pinned via manifest "key") or installed from the Web Store
|
|
20
|
+
// (different ID).
|
|
21
|
+
import { CAPTURED_PARAM } from './install-capture.js';
|
|
22
|
+
const REDIRECT_URL = 'https://github.com/jshute96/SourceMonkey/blob/main/docs/redirect-page.md';
|
|
23
|
+
const ALLOWED_PAGES = new Set(['collections.html', 'options.html', 'userscript.html']);
|
|
24
|
+
// Pure: maps a source URL to its rewrite target, or null when the URL
|
|
25
|
+
// is not a deep link we should act on. Exported for unit tests.
|
|
26
|
+
export function buildDeepLinkTarget(srcUrl) {
|
|
27
|
+
let u;
|
|
28
|
+
try {
|
|
29
|
+
u = new URL(srcUrl);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Reject userinfo (user:pass@host) for defence in depth — origin
|
|
35
|
+
// strips it, so the equality check would otherwise accept it.
|
|
36
|
+
if (u.username !== '' || u.password !== '')
|
|
37
|
+
return null;
|
|
38
|
+
if (`${u.origin}${u.pathname}` !== REDIRECT_URL)
|
|
39
|
+
return null;
|
|
40
|
+
const page = u.searchParams.get('page');
|
|
41
|
+
if (!page || !ALLOWED_PAGES.has(page))
|
|
42
|
+
return null;
|
|
43
|
+
const passthrough = new URLSearchParams(u.searchParams);
|
|
44
|
+
passthrough.delete('page');
|
|
45
|
+
// `captured` tells the viewer "the install capture opened this",
|
|
46
|
+
// which lets it navigate the tab away on its own. That claim is only
|
|
47
|
+
// the service worker's to make — this is the one way a web page
|
|
48
|
+
// could put a param onto an extension page, so it does not carry.
|
|
49
|
+
passthrough.delete(CAPTURED_PARAM);
|
|
50
|
+
// Note: passthrough.toString() uses application/x-www-form-urlencoded
|
|
51
|
+
// encoding (spaces → `+`, literal `+` → `%2B`). End-to-end loss-less
|
|
52
|
+
// for consumers that decode via URLSearchParams; consumers that
|
|
53
|
+
// hand-parse the query should be aware of this round-trip.
|
|
54
|
+
const qs = passthrough.toString();
|
|
55
|
+
return chrome.runtime.getURL(page) + (qs ? `?${qs}` : '');
|
|
56
|
+
}
|
|
57
|
+
export function installDeepLinkRedirect() {
|
|
58
|
+
const wn = chrome.webNavigation;
|
|
59
|
+
if (!wn) {
|
|
60
|
+
console.info('[SourceMonkey] chrome.webNavigation not available; deep-link redirect disabled');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
wn.onBeforeNavigate.addListener((d) => {
|
|
64
|
+
if (d.frameId !== 0)
|
|
65
|
+
return;
|
|
66
|
+
const target = buildDeepLinkTarget(d.url);
|
|
67
|
+
if (!target)
|
|
68
|
+
return;
|
|
69
|
+
// Tab may have closed between the event firing and our update
|
|
70
|
+
// landing; ignore the resulting rejection.
|
|
71
|
+
chrome.tabs.update(d.tabId, { url: target }).catch(() => { });
|
|
72
|
+
},
|
|
73
|
+
// Cheap pre-filter so we don't run on every navigation. The
|
|
74
|
+
// listener does the authoritative check; this just narrows the
|
|
75
|
+
// event volume.
|
|
76
|
+
{ url: [{ urlPrefix: REDIRECT_URL }] });
|
|
77
|
+
}
|