@zucker-framework/ext-kit 1.0.2 → 1.0.5
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/README.md +49 -0
- package/assets/offscreen.js +20 -4
- package/dist/ext-kit-popup.js +438 -154
- package/dist/ext-kit.js +1605 -663
- package/dist/index.d.mts +132 -31
- package/dist/index.d.ts +132 -31
- package/dist/index.js +2075 -956
- package/dist/index.mjs +2069 -956
- package/dist/scroll-capture.js +309 -0
- package/package.json +4 -2
package/dist/ext-kit-popup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
// src/popup.
|
|
3
|
+
// src/popup.js
|
|
4
4
|
function initPopup(config) {
|
|
5
5
|
const byId = (id) => document.getElementById(id);
|
|
6
6
|
const el = {
|
|
@@ -36,35 +36,94 @@
|
|
|
36
36
|
licenseCode: byId("licenseCode"),
|
|
37
37
|
activateBtn: byId("activateBtn"),
|
|
38
38
|
deactivateBtn: byId("deactivateBtn"),
|
|
39
|
+
recheckLicense: byId("recheckLicense"),
|
|
40
|
+
licenseHint: byId("licenseHint"),
|
|
39
41
|
status: byId("status")
|
|
40
42
|
};
|
|
41
43
|
let pollTimer = null;
|
|
44
|
+
let disposed = false;
|
|
45
|
+
window.addEventListener("pagehide", () => {
|
|
46
|
+
disposed = true;
|
|
47
|
+
pollGeneration++;
|
|
48
|
+
clearTimeout(pollTimer);
|
|
49
|
+
if (scanJob) {
|
|
50
|
+
const job = scanJob;
|
|
51
|
+
scanJob = null;
|
|
52
|
+
void chrome.scripting.executeScript({
|
|
53
|
+
target: { tabId: job.tabId },
|
|
54
|
+
func: collectSameOriginLinks,
|
|
55
|
+
args: [{ phase: "cancel", id: job.id }]
|
|
56
|
+
}).catch(() => {
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}, { once: true });
|
|
42
60
|
let queue = [];
|
|
43
61
|
let queueLocked = false;
|
|
62
|
+
let operationBusy = false;
|
|
63
|
+
let editingIndex = -1;
|
|
64
|
+
let scanJob = null;
|
|
65
|
+
let pollGeneration = 0;
|
|
66
|
+
const settingsKey = config.settingsKey || config.queueKey.replace(/_url_queue$/, "_settings");
|
|
67
|
+
const settingFields = () => document.querySelectorAll(
|
|
68
|
+
".settings select, .settings input[data-setting]"
|
|
69
|
+
);
|
|
70
|
+
let savedSettings = {};
|
|
44
71
|
const prefix = config.messagePrefix;
|
|
45
|
-
void init();
|
|
72
|
+
void init().catch((error) => setStatus(errText(error), true));
|
|
46
73
|
async function init() {
|
|
47
74
|
el.urls.addEventListener("input", updateUrlCount);
|
|
48
75
|
el.addUrls.addEventListener("click", addInputUrls);
|
|
49
76
|
el.scanSite.addEventListener("click", scanCurrentSite);
|
|
50
77
|
el.selectAll.addEventListener("click", () => setAllSelected(true));
|
|
51
78
|
el.selectNone.addEventListener("click", () => setAllSelected(false));
|
|
52
|
-
el.moreQueueActions.addEventListener("click", () =>
|
|
79
|
+
el.moreQueueActions.addEventListener("click", (event) => {
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
setQueueMenu(el.queueMenu.classList.contains("hidden"));
|
|
82
|
+
});
|
|
83
|
+
document.addEventListener("click", (event) => {
|
|
84
|
+
if (!el.queueMenu.classList.contains("hidden") && !el.queueMenu.contains(event.target))
|
|
85
|
+
setQueueMenu(false);
|
|
86
|
+
});
|
|
87
|
+
document.addEventListener("keydown", (event) => {
|
|
88
|
+
if (event.key === "Escape" && !el.queueMenu.classList.contains("hidden")) {
|
|
89
|
+
setQueueMenu(false);
|
|
90
|
+
el.moreQueueActions.focus();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
53
93
|
el.removeUnselected.addEventListener("click", removeUnselected);
|
|
54
94
|
el.clearQueue.addEventListener("click", clearQueue);
|
|
55
95
|
el.captureCurrent.addEventListener("click", quickCapture);
|
|
56
96
|
el.start.addEventListener("click", startBatch);
|
|
57
97
|
el.cancel.addEventListener("click", cancelBatch);
|
|
58
98
|
el.clearBatch.addEventListener("click", clearBatchResults);
|
|
59
|
-
el.skipVerify.addEventListener(
|
|
60
|
-
|
|
99
|
+
el.skipVerify.addEventListener(
|
|
100
|
+
"click",
|
|
101
|
+
() => send({ type: `${prefix}_SKIP_VERIFY` })
|
|
102
|
+
);
|
|
103
|
+
el.haveCode.addEventListener(
|
|
104
|
+
"click",
|
|
105
|
+
() => el.licensePanel.classList.toggle("hidden")
|
|
106
|
+
);
|
|
61
107
|
el.activateBtn.addEventListener("click", activateLicense);
|
|
62
108
|
el.deactivateBtn.addEventListener("click", deactivateLicense);
|
|
63
|
-
|
|
109
|
+
el.recheckLicense?.addEventListener("click", recheckLicense);
|
|
110
|
+
el.start.disabled = true;
|
|
111
|
+
el.captureCurrent.disabled = true;
|
|
112
|
+
await Promise.all([loadQueue(), loadSettings()]);
|
|
113
|
+
config.onSettingsLoaded?.();
|
|
114
|
+
settingFields().forEach((field) => {
|
|
115
|
+
field.addEventListener("change", saveSettings);
|
|
116
|
+
if (field.type === "number")
|
|
117
|
+
field.addEventListener("input", saveSettings);
|
|
118
|
+
});
|
|
64
119
|
updateUrlCount();
|
|
65
120
|
renderQueue();
|
|
66
121
|
await refreshState();
|
|
67
122
|
}
|
|
123
|
+
function setQueueMenu(open) {
|
|
124
|
+
el.queueMenu.classList.toggle("hidden", !open);
|
|
125
|
+
el.moreQueueActions.setAttribute("aria-expanded", String(open));
|
|
126
|
+
}
|
|
68
127
|
async function activateLicense() {
|
|
69
128
|
const code = el.licenseCode.value.trim();
|
|
70
129
|
if (!code) {
|
|
@@ -85,11 +144,29 @@
|
|
|
85
144
|
setStatus("Pro activated \u2014 enjoy unlimited pages!");
|
|
86
145
|
}
|
|
87
146
|
async function deactivateLicense() {
|
|
147
|
+
el.deactivateBtn.disabled = true;
|
|
88
148
|
const response = await send({ type: `${prefix}_CLEAR_LICENSE` });
|
|
149
|
+
el.deactivateBtn.disabled = false;
|
|
89
150
|
if (response.ok) {
|
|
90
151
|
renderQuota(response.quota);
|
|
91
|
-
setStatus("
|
|
92
|
-
}
|
|
152
|
+
setStatus("Device released. Its license slot is now available.");
|
|
153
|
+
} else setStatus(response.error || "Could not release this device", true);
|
|
154
|
+
}
|
|
155
|
+
async function recheckLicense() {
|
|
156
|
+
el.recheckLicense.disabled = true;
|
|
157
|
+
setStatus("Checking your Pro license\u2026");
|
|
158
|
+
const response = await send({ type: `${prefix}_REFRESH_LICENSE` });
|
|
159
|
+
el.recheckLicense.disabled = false;
|
|
160
|
+
if (!response.ok)
|
|
161
|
+
return setStatus(
|
|
162
|
+
response.error || "Could not check your license",
|
|
163
|
+
true
|
|
164
|
+
);
|
|
165
|
+
renderQuota(response.quota);
|
|
166
|
+
setStatus(
|
|
167
|
+
response.quota.pro ? "Pro confirmed \u2014 unlimited exports." : response.quota.licenseMessage || "Enter your original activation code to restore Pro in this extension.",
|
|
168
|
+
!response.quota.pro
|
|
169
|
+
);
|
|
93
170
|
}
|
|
94
171
|
function parseUrls() {
|
|
95
172
|
return normalizeInputUrls(el.urls.value.split(/\s+/));
|
|
@@ -106,7 +183,6 @@
|
|
|
106
183
|
if (!/^https?:$/.test(url.protocol)) {
|
|
107
184
|
continue;
|
|
108
185
|
}
|
|
109
|
-
url.hash = "";
|
|
110
186
|
const normalized = url.toString();
|
|
111
187
|
if (!seen.has(normalized)) {
|
|
112
188
|
seen.add(normalized);
|
|
@@ -147,30 +223,60 @@
|
|
|
147
223
|
return added;
|
|
148
224
|
}
|
|
149
225
|
async function scanCurrentSite() {
|
|
150
|
-
|
|
226
|
+
if (scanJob) {
|
|
227
|
+
const stopped = scanJob;
|
|
228
|
+
scanJob = null;
|
|
229
|
+
await chrome.scripting.executeScript({
|
|
230
|
+
target: { tabId: stopped.tabId },
|
|
231
|
+
func: collectSameOriginLinks,
|
|
232
|
+
args: [{ phase: "cancel", id: stopped.id }]
|
|
233
|
+
}).catch(() => {
|
|
234
|
+
});
|
|
235
|
+
el.scanSite.textContent = "\u2316 Scan site";
|
|
236
|
+
setStatus("Scan stopped; imported links are kept");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (queueLocked || operationBusy) return;
|
|
240
|
+
let job;
|
|
151
241
|
try {
|
|
152
|
-
const [tab] = await chrome.tabs.query({
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
242
|
+
const [tab] = await chrome.tabs.query({
|
|
243
|
+
active: true,
|
|
244
|
+
currentWindow: true
|
|
245
|
+
});
|
|
246
|
+
if (!tab?.id || !/^https?:\/\//i.test(tab.url || ""))
|
|
247
|
+
throw new Error("Open an http(s) website first");
|
|
248
|
+
job = { tabId: tab.id, id: crypto.randomUUID() };
|
|
249
|
+
scanJob = job;
|
|
250
|
+
el.scanSite.textContent = "Stop scan";
|
|
251
|
+
const [links] = await chrome.scripting.executeScript({
|
|
157
252
|
target: { tabId: tab.id },
|
|
158
|
-
func: collectSameOriginLinks
|
|
253
|
+
func: collectSameOriginLinks,
|
|
254
|
+
args: [{ phase: "links" }]
|
|
159
255
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
256
|
+
if (scanJob !== job) return;
|
|
257
|
+
const added = addUrlsToQueue(links?.result?.urls || []);
|
|
258
|
+
setStatus(`Added ${added} page links; checking sitemap\u2026`);
|
|
259
|
+
const [maps] = await chrome.scripting.executeScript({
|
|
260
|
+
target: { tabId: tab.id },
|
|
261
|
+
func: collectSameOriginLinks,
|
|
262
|
+
args: [{ phase: "sitemap", id: job.id }]
|
|
263
|
+
});
|
|
264
|
+
if (scanJob !== job) return;
|
|
265
|
+
if (queueLocked || operationBusy) {
|
|
266
|
+
setStatus("Scan stopped when the task started; page links are kept");
|
|
167
267
|
return;
|
|
168
268
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
269
|
+
const extra = addUrlsToQueue(maps?.result?.urls || []);
|
|
270
|
+
setStatus(
|
|
271
|
+
`Added ${added + extra} URLs${maps?.result?.incomplete ? "; some sitemaps could not be read" : ""}`
|
|
272
|
+
);
|
|
172
273
|
} catch (error) {
|
|
173
274
|
setStatus(errText(error), true);
|
|
275
|
+
} finally {
|
|
276
|
+
if (scanJob === job) {
|
|
277
|
+
scanJob = null;
|
|
278
|
+
el.scanSite.textContent = "\u2316 Scan site";
|
|
279
|
+
}
|
|
174
280
|
}
|
|
175
281
|
}
|
|
176
282
|
async function startBatch() {
|
|
@@ -179,14 +285,21 @@
|
|
|
179
285
|
setStatus("Select at least one URL from the queue", true);
|
|
180
286
|
return;
|
|
181
287
|
}
|
|
182
|
-
|
|
288
|
+
operationBusy = true;
|
|
289
|
+
renderQueue();
|
|
183
290
|
setStatus("Starting\u2026");
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
291
|
+
let response;
|
|
292
|
+
try {
|
|
293
|
+
response = await send({
|
|
294
|
+
type: `${prefix}_START`,
|
|
295
|
+
urls,
|
|
296
|
+
settings: await config.collectSettings()
|
|
297
|
+
});
|
|
298
|
+
} catch (error) {
|
|
299
|
+
response = { ok: false, error: errText(error) };
|
|
300
|
+
}
|
|
301
|
+
operationBusy = false;
|
|
302
|
+
renderQueue();
|
|
190
303
|
if (!response.ok) {
|
|
191
304
|
setStatus(response.error || "Failed to start", true);
|
|
192
305
|
return;
|
|
@@ -199,20 +312,30 @@
|
|
|
199
312
|
);
|
|
200
313
|
}
|
|
201
314
|
async function quickCapture() {
|
|
202
|
-
const [tab] = await chrome.tabs.query({
|
|
315
|
+
const [tab] = await chrome.tabs.query({
|
|
316
|
+
active: true,
|
|
317
|
+
currentWindow: true
|
|
318
|
+
});
|
|
203
319
|
if (!tab?.id || !/^https?:\/\//i.test(tab.url || "")) {
|
|
204
320
|
setStatus("Open an http(s) website first", true);
|
|
205
321
|
return;
|
|
206
322
|
}
|
|
207
|
-
|
|
208
|
-
|
|
323
|
+
operationBusy = true;
|
|
324
|
+
renderQueue();
|
|
325
|
+
el.cancel.classList.remove("hidden");
|
|
209
326
|
setStatus(config.texts.quickIng);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
327
|
+
let response;
|
|
328
|
+
try {
|
|
329
|
+
response = await send({
|
|
330
|
+
type: `${prefix}_${config.quickType}`,
|
|
331
|
+
tabId: tab.id,
|
|
332
|
+
settings: await config.collectQuickSettings()
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
response = { ok: false, error: errText(error) };
|
|
336
|
+
}
|
|
337
|
+
operationBusy = false;
|
|
338
|
+
await refreshState();
|
|
216
339
|
renderQueue();
|
|
217
340
|
if (!response.ok || !response.result) {
|
|
218
341
|
setStatus(response.error || config.texts.quickFailed, true);
|
|
@@ -235,6 +358,7 @@
|
|
|
235
358
|
}
|
|
236
359
|
renderBatch(response.batch);
|
|
237
360
|
setStatus("Batch results cleared");
|
|
361
|
+
renderQuota(response.quota);
|
|
238
362
|
}
|
|
239
363
|
async function refreshState() {
|
|
240
364
|
const response = await send({ type: `${prefix}_STATE` });
|
|
@@ -243,54 +367,61 @@
|
|
|
243
367
|
}
|
|
244
368
|
renderQuota(response.quota);
|
|
245
369
|
renderBatch(response.batch);
|
|
246
|
-
if (response.batch?.running) {
|
|
370
|
+
if (response.batch?.running || response.batch?.busy || response.quota?.licenseRefreshing) {
|
|
247
371
|
schedulePoll();
|
|
248
372
|
}
|
|
249
373
|
}
|
|
250
374
|
function schedulePoll() {
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
375
|
+
if (disposed) return;
|
|
376
|
+
clearTimeout(pollTimer);
|
|
377
|
+
const generation = ++pollGeneration;
|
|
378
|
+
async function poll() {
|
|
255
379
|
const response = await send({ type: `${prefix}_STATE` });
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
renderBatch(response.batch);
|
|
261
|
-
if (!response.batch?.running && pollTimer) {
|
|
262
|
-
clearInterval(pollTimer);
|
|
380
|
+
if (disposed || generation !== pollGeneration) return;
|
|
381
|
+
if (response.ok) {
|
|
382
|
+
renderQuota(response.quota);
|
|
383
|
+
renderBatch(response.batch);
|
|
263
384
|
}
|
|
264
|
-
|
|
385
|
+
if (!response.ok || response.batch?.running || response.batch?.busy || response.quota?.licenseRefreshing)
|
|
386
|
+
pollTimer = setTimeout(poll, 800);
|
|
387
|
+
}
|
|
388
|
+
pollTimer = setTimeout(poll, 800);
|
|
265
389
|
}
|
|
266
390
|
function renderQuota(quota) {
|
|
267
391
|
if (!quota) {
|
|
268
392
|
return;
|
|
269
393
|
}
|
|
394
|
+
if (el.licenseHint)
|
|
395
|
+
el.licenseHint.textContent = quota.licenseMessage || (quota.pro ? "Your Pro license includes unlimited exports." : "Already purchased? Enter your original activation code to restore Pro in this extension and browser.");
|
|
396
|
+
el.recheckLicense?.classList.toggle("hidden", !quota.hasLicense);
|
|
397
|
+
el.proLink.classList.toggle("hidden", quota.pro || quota.hasLicense);
|
|
398
|
+
el.deactivateBtn.classList.toggle("hidden", !quota.hasLicense);
|
|
399
|
+
el.activateBtn.classList.toggle("hidden", quota.pro);
|
|
270
400
|
if (quota.pro) {
|
|
271
401
|
const until = quota.expiresAt ? ` \xB7 ${String(quota.expiresAt).slice(0, 10)}` : "";
|
|
272
402
|
el.quotaText.textContent = `unlimited${until}`;
|
|
273
|
-
|
|
274
|
-
el.
|
|
275
|
-
el.activateBtn.classList.add("hidden");
|
|
403
|
+
} else if (quota.hasLicense) {
|
|
404
|
+
el.quotaText.textContent = quota.licenseStatus === "checking" ? "Checking Pro\u2026" : "Pro needs verification";
|
|
276
405
|
} else {
|
|
277
406
|
el.quotaText.textContent = `${quota.remaining}/${quota.limit} today`;
|
|
278
|
-
el.proLink.classList.remove("hidden");
|
|
279
|
-
el.deactivateBtn.classList.add("hidden");
|
|
280
|
-
el.activateBtn.classList.remove("hidden");
|
|
281
407
|
}
|
|
282
408
|
}
|
|
283
409
|
function renderBatch(batch) {
|
|
284
410
|
if (!batch) {
|
|
285
411
|
return;
|
|
286
412
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
el.
|
|
290
|
-
|
|
291
|
-
|
|
413
|
+
const busy = batch.running || batch.busy;
|
|
414
|
+
const stalled = Boolean(batch.stalled);
|
|
415
|
+
el.start.classList.toggle("hidden", busy);
|
|
416
|
+
el.captureCurrent.classList.toggle("hidden", busy);
|
|
417
|
+
el.cancel.classList.toggle("hidden", !busy);
|
|
418
|
+
queueLocked = busy && !stalled;
|
|
419
|
+
renderQueue(true);
|
|
292
420
|
el.progressWrap.classList.toggle("hidden", !batch.total);
|
|
293
|
-
el.clearBatch.classList.toggle(
|
|
421
|
+
el.clearBatch.classList.toggle(
|
|
422
|
+
"hidden",
|
|
423
|
+
busy && !stalled || !batch.total
|
|
424
|
+
);
|
|
294
425
|
if (batch.total) {
|
|
295
426
|
el.progressText.textContent = `${batch.done} / ${batch.total}`;
|
|
296
427
|
el.progressCurrent.textContent = batch.current || "";
|
|
@@ -307,7 +438,14 @@
|
|
|
307
438
|
rowEl.className = "resultItem";
|
|
308
439
|
const state = document.createElement("span");
|
|
309
440
|
state.className = `state ${item.status}`;
|
|
310
|
-
state.textContent = {
|
|
441
|
+
state.textContent = {
|
|
442
|
+
pending: "\xB7",
|
|
443
|
+
running: "\u2026",
|
|
444
|
+
downloading: "\u2193",
|
|
445
|
+
cancelled: "\u2013",
|
|
446
|
+
success: "\u2713",
|
|
447
|
+
failed: "\u2717"
|
|
448
|
+
}[item.status] || "\xB7";
|
|
311
449
|
const url = document.createElement("span");
|
|
312
450
|
url.className = "url";
|
|
313
451
|
url.textContent = item.url.replace(/^https?:\/\//, "");
|
|
@@ -326,7 +464,7 @@
|
|
|
326
464
|
}
|
|
327
465
|
el.resultList.append(rowEl);
|
|
328
466
|
}
|
|
329
|
-
if (batch.folder && batch.
|
|
467
|
+
if (batch.folder && batch.success > 0) {
|
|
330
468
|
el.folderNote.classList.remove("hidden");
|
|
331
469
|
el.folderNote.textContent = `Saved to Downloads/${batch.folder}/`;
|
|
332
470
|
} else {
|
|
@@ -334,25 +472,52 @@
|
|
|
334
472
|
el.folderNote.textContent = "";
|
|
335
473
|
}
|
|
336
474
|
}
|
|
475
|
+
async function loadSettings() {
|
|
476
|
+
const data = await chrome.storage.local.get(settingsKey);
|
|
477
|
+
const saved = data[settingsKey];
|
|
478
|
+
if (!saved || typeof saved !== "object") return;
|
|
479
|
+
settingFields().forEach((field) => {
|
|
480
|
+
const value = String(saved[field.id]);
|
|
481
|
+
if (field.tagName === "SELECT" && [...field.options].some((option) => option.value === value))
|
|
482
|
+
field.value = value;
|
|
483
|
+
if (field.type === "number" && value.trim() && Number.isFinite(Number(value))) {
|
|
484
|
+
const fallback = field.value;
|
|
485
|
+
field.value = value;
|
|
486
|
+
if (!field.checkValidity()) field.value = fallback;
|
|
487
|
+
}
|
|
488
|
+
savedSettings[field.id] = field.value;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function saveSettings() {
|
|
492
|
+
settingFields().forEach((field) => {
|
|
493
|
+
if (field.type !== "number" || field.value.trim() && Number.isFinite(field.valueAsNumber) && field.checkValidity())
|
|
494
|
+
savedSettings[field.id] = field.value;
|
|
495
|
+
});
|
|
496
|
+
void chrome.storage.local.set({ [settingsKey]: { ...savedSettings } }).catch((error) => setStatus(errText(error), true));
|
|
497
|
+
}
|
|
337
498
|
async function loadQueue() {
|
|
338
499
|
const data = await chrome.storage.local.get(config.queueKey);
|
|
339
500
|
const saved = Array.isArray(data[config.queueKey]) ? data[config.queueKey] : [];
|
|
340
501
|
queue = saved.filter((item) => item && typeof item.url === "string").slice(0, 500).map((item) => ({ url: item.url, selected: item.selected !== false }));
|
|
341
502
|
}
|
|
342
503
|
function saveQueue() {
|
|
343
|
-
void chrome.storage.local.set({ [config.queueKey]: queue });
|
|
504
|
+
void chrome.storage.local.set({ [config.queueKey]: queue }).catch((error) => setStatus(errText(error), true));
|
|
344
505
|
}
|
|
345
|
-
function renderQueue() {
|
|
506
|
+
function renderQueue(preserveEditor = false) {
|
|
507
|
+
if (queueLocked || operationBusy) editingIndex = -1;
|
|
508
|
+
if (editingIndex >= queue.length) editingIndex = -1;
|
|
346
509
|
const selected = queue.filter((item) => item.selected).length;
|
|
347
510
|
el.queueSummary.textContent = `${selected}/${queue.length} selected`;
|
|
348
511
|
el.start.textContent = selected ? config.texts.startSome(selected) : config.texts.startNone;
|
|
349
|
-
el.start.disabled = selected === 0;
|
|
350
|
-
el.selectAll.disabled = queueLocked || !queue.length;
|
|
351
|
-
el.selectNone.disabled = queueLocked || !queue.length;
|
|
352
|
-
el.moreQueueActions.disabled = queueLocked || !queue.length;
|
|
353
|
-
el.addUrls.disabled = queueLocked;
|
|
354
|
-
el.scanSite.disabled = queueLocked;
|
|
355
|
-
el.captureCurrent.disabled = queueLocked;
|
|
512
|
+
el.start.disabled = operationBusy || queueLocked || selected === 0;
|
|
513
|
+
el.selectAll.disabled = queueLocked || operationBusy || !queue.length;
|
|
514
|
+
el.selectNone.disabled = queueLocked || operationBusy || !queue.length;
|
|
515
|
+
el.moreQueueActions.disabled = queueLocked || operationBusy || !queue.length;
|
|
516
|
+
el.addUrls.disabled = queueLocked || operationBusy;
|
|
517
|
+
el.scanSite.disabled = queueLocked || operationBusy;
|
|
518
|
+
el.captureCurrent.disabled = queueLocked || operationBusy;
|
|
519
|
+
if (preserveEditor && editingIndex >= 0 && el.queueList.querySelector(".queueEdit"))
|
|
520
|
+
return;
|
|
356
521
|
el.queueList.replaceChildren();
|
|
357
522
|
if (!queue.length) {
|
|
358
523
|
const empty = document.createElement("p");
|
|
@@ -367,28 +532,23 @@
|
|
|
367
532
|
const checkbox = document.createElement("input");
|
|
368
533
|
checkbox.type = "checkbox";
|
|
369
534
|
checkbox.checked = item.selected;
|
|
370
|
-
checkbox.disabled = queueLocked;
|
|
535
|
+
checkbox.disabled = queueLocked || operationBusy;
|
|
371
536
|
checkbox.setAttribute("aria-label", `Include ${item.url}`);
|
|
372
537
|
checkbox.addEventListener("change", () => {
|
|
373
538
|
item.selected = checkbox.checked;
|
|
374
539
|
saveQueue();
|
|
375
540
|
renderQueue();
|
|
376
541
|
});
|
|
377
|
-
const url =
|
|
378
|
-
url.type = "button";
|
|
379
|
-
url.className = "queueUrl";
|
|
380
|
-
url.textContent = item.url.replace(/^https?:\/\//, "");
|
|
381
|
-
url.title = item.url;
|
|
382
|
-
url.disabled = queueLocked;
|
|
383
|
-
url.addEventListener("click", () => editQueueItem(index));
|
|
542
|
+
const url = index === editingIndex ? buildQueueEditor(item, index) : buildQueueLabel(item, index);
|
|
384
543
|
const remove = document.createElement("button");
|
|
385
544
|
remove.type = "button";
|
|
386
545
|
remove.className = "queueRemove";
|
|
387
546
|
remove.textContent = "\xD7";
|
|
388
547
|
remove.title = "Remove URL";
|
|
389
|
-
remove.disabled = queueLocked;
|
|
548
|
+
remove.disabled = queueLocked || operationBusy;
|
|
390
549
|
remove.addEventListener("click", () => {
|
|
391
550
|
queue.splice(index, 1);
|
|
551
|
+
editingIndex = -1;
|
|
392
552
|
saveQueue();
|
|
393
553
|
renderQueue();
|
|
394
554
|
});
|
|
@@ -396,27 +556,80 @@
|
|
|
396
556
|
el.queueList.append(row);
|
|
397
557
|
});
|
|
398
558
|
}
|
|
399
|
-
function
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
559
|
+
function buildQueueLabel(item, index) {
|
|
560
|
+
const url = document.createElement("button");
|
|
561
|
+
url.type = "button";
|
|
562
|
+
url.className = "queueUrl";
|
|
563
|
+
url.textContent = item.url.replace(/^https?:\/\//, "");
|
|
564
|
+
url.title = `${item.url}
|
|
565
|
+
Click to edit`;
|
|
566
|
+
url.disabled = queueLocked || operationBusy;
|
|
567
|
+
url.addEventListener("click", () => {
|
|
568
|
+
if (queueLocked || operationBusy) return;
|
|
569
|
+
editingIndex = index;
|
|
570
|
+
renderQueue();
|
|
571
|
+
});
|
|
572
|
+
return url;
|
|
573
|
+
}
|
|
574
|
+
function buildQueueEditor(item, index) {
|
|
575
|
+
const input = document.createElement("input");
|
|
576
|
+
input.type = "text";
|
|
577
|
+
input.className = "queueEdit";
|
|
578
|
+
input.value = item.url;
|
|
579
|
+
input.spellcheck = false;
|
|
580
|
+
input.setAttribute("aria-label", `Edit ${item.url}`);
|
|
581
|
+
const leaveEditor = () => {
|
|
582
|
+
editingIndex = -1;
|
|
583
|
+
const label = buildQueueLabel(item, index);
|
|
584
|
+
input.parentElement?.querySelector('input[type="checkbox"]')?.setAttribute("aria-label", `Include ${item.url}`);
|
|
585
|
+
input.replaceWith(label);
|
|
586
|
+
return label;
|
|
587
|
+
};
|
|
588
|
+
const finish = (fromBlur) => {
|
|
589
|
+
if (editingIndex !== index) return;
|
|
590
|
+
const [normalized] = normalizeInputUrls([input.value]);
|
|
591
|
+
const duplicate = normalized && queue.some(
|
|
592
|
+
(other, otherIndex) => otherIndex !== index && other.url === normalized
|
|
593
|
+
);
|
|
594
|
+
if (!normalized || duplicate) {
|
|
595
|
+
const message = duplicate ? "That URL is already in the queue" : "Enter a valid http(s) URL";
|
|
596
|
+
if (fromBlur) {
|
|
597
|
+
setStatus(`${message} \u2014 edit discarded`, true);
|
|
598
|
+
leaveEditor();
|
|
599
|
+
} else {
|
|
600
|
+
setStatus(message, true);
|
|
601
|
+
input.focus();
|
|
602
|
+
input.select();
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (normalized !== item.url) {
|
|
607
|
+
item.url = normalized;
|
|
608
|
+
saveQueue();
|
|
609
|
+
setStatus("URL updated");
|
|
610
|
+
}
|
|
611
|
+
const label = leaveEditor();
|
|
612
|
+
if (!fromBlur) label.focus();
|
|
613
|
+
};
|
|
614
|
+
input.addEventListener("keydown", (event) => {
|
|
615
|
+
if (event.key === "Enter") {
|
|
616
|
+
event.preventDefault();
|
|
617
|
+
finish(false);
|
|
618
|
+
} else if (event.key === "Escape") {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
event.stopPropagation();
|
|
621
|
+
leaveEditor().focus();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
input.addEventListener("blur", () => finish(true));
|
|
625
|
+
queueMicrotask(() => {
|
|
626
|
+
input.focus();
|
|
627
|
+
input.select();
|
|
628
|
+
});
|
|
629
|
+
return input;
|
|
417
630
|
}
|
|
418
631
|
function setAllSelected(selected) {
|
|
419
|
-
if (queueLocked) {
|
|
632
|
+
if (queueLocked || operationBusy) {
|
|
420
633
|
return;
|
|
421
634
|
}
|
|
422
635
|
queue.forEach((item) => {
|
|
@@ -426,14 +639,18 @@
|
|
|
426
639
|
renderQueue();
|
|
427
640
|
}
|
|
428
641
|
function removeUnselected() {
|
|
642
|
+
if (queueLocked || operationBusy) return;
|
|
429
643
|
queue = queue.filter((item) => item.selected);
|
|
430
|
-
|
|
644
|
+
editingIndex = -1;
|
|
645
|
+
setQueueMenu(false);
|
|
431
646
|
saveQueue();
|
|
432
647
|
renderQueue();
|
|
433
648
|
}
|
|
434
649
|
function clearQueue() {
|
|
650
|
+
if (queueLocked || operationBusy) return;
|
|
435
651
|
queue = [];
|
|
436
|
-
|
|
652
|
+
editingIndex = -1;
|
|
653
|
+
setQueueMenu(false);
|
|
437
654
|
saveQueue();
|
|
438
655
|
renderQueue();
|
|
439
656
|
}
|
|
@@ -448,6 +665,7 @@
|
|
|
448
665
|
});
|
|
449
666
|
}
|
|
450
667
|
function setStatus(text, isError = false) {
|
|
668
|
+
if (disposed) return;
|
|
451
669
|
el.status.textContent = text;
|
|
452
670
|
el.status.classList.toggle("error", isError);
|
|
453
671
|
}
|
|
@@ -455,69 +673,135 @@
|
|
|
455
673
|
return error instanceof Error ? error.message : String(error);
|
|
456
674
|
}
|
|
457
675
|
}
|
|
458
|
-
async function collectSameOriginLinks() {
|
|
459
|
-
const seen = /* @__PURE__ */ new Set();
|
|
460
|
-
const out = [];
|
|
461
|
-
const origin = location.origin;
|
|
462
|
-
let sitemapCount = 0;
|
|
676
|
+
async function collectSameOriginLinks(options = {}) {
|
|
677
|
+
const origin = location.origin, seen = /* @__PURE__ */ new Set(), urls = [];
|
|
463
678
|
const add = (value) => {
|
|
464
679
|
try {
|
|
465
680
|
const url = new URL(value || "", location.href);
|
|
466
|
-
url.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
out.push(url.toString());
|
|
681
|
+
if (url.origin === origin && /^https?:$/.test(url.protocol) && !seen.has(url.href) && urls.length < 500) {
|
|
682
|
+
seen.add(url.href);
|
|
683
|
+
urls.push(url.href);
|
|
470
684
|
return true;
|
|
471
685
|
}
|
|
472
686
|
} catch {
|
|
473
687
|
}
|
|
474
688
|
return false;
|
|
475
689
|
};
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
const sitemapQueue = [new URL("/sitemap.xml", origin).toString()];
|
|
484
|
-
const visitedSitemaps = /* @__PURE__ */ new Set();
|
|
485
|
-
while (sitemapQueue.length && visitedSitemaps.size < 8 && out.length < 500) {
|
|
486
|
-
const sitemapUrl = sitemapQueue.shift();
|
|
487
|
-
if (visitedSitemaps.has(sitemapUrl)) {
|
|
488
|
-
continue;
|
|
690
|
+
if (options.phase === "cancel") {
|
|
691
|
+
const active = globalThis.__zuckerExtScan;
|
|
692
|
+
if (active?.id === options.id) {
|
|
693
|
+
active.cancelled = true;
|
|
694
|
+
active.controller?.abort();
|
|
489
695
|
}
|
|
490
|
-
|
|
696
|
+
return { urls: [], cancelled: true };
|
|
697
|
+
}
|
|
698
|
+
if (options.phase !== "sitemap") {
|
|
699
|
+
add(location.href);
|
|
700
|
+
for (const a of document.querySelectorAll("a[href]"))
|
|
701
|
+
add(a.getAttribute("href"));
|
|
702
|
+
}
|
|
703
|
+
if (options.phase === "links") return { urls, sitemapCount: 0 };
|
|
704
|
+
const scan = { id: options.id, cancelled: false, controller: null };
|
|
705
|
+
globalThis.__zuckerExtScan?.controller?.abort();
|
|
706
|
+
globalThis.__zuckerExtScan = scan;
|
|
707
|
+
const deadline = Date.now() + 6e3;
|
|
708
|
+
let incomplete = false, sitemapCount = 0;
|
|
709
|
+
const queue = [new URL("/sitemap.xml", origin).href], visited = /* @__PURE__ */ new Set();
|
|
710
|
+
const sameOrigin = (value) => {
|
|
491
711
|
try {
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
712
|
+
const url = new URL(value, origin);
|
|
713
|
+
return url.origin === origin && /^https?:$/.test(url.protocol) ? url.href : null;
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
async function read(url) {
|
|
719
|
+
if (scan.cancelled || Date.now() >= deadline) {
|
|
720
|
+
incomplete = true;
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
const controller = new AbortController();
|
|
724
|
+
scan.controller = controller;
|
|
725
|
+
const timer = setTimeout(
|
|
726
|
+
() => controller.abort(),
|
|
727
|
+
Math.min(1500, deadline - Date.now())
|
|
728
|
+
);
|
|
729
|
+
try {
|
|
730
|
+
const response = await fetch(url, {
|
|
731
|
+
credentials: "same-origin",
|
|
732
|
+
signal: controller.signal
|
|
733
|
+
});
|
|
734
|
+
if (!response.ok) return null;
|
|
735
|
+
if (Number(response.headers.get("content-length")) > 2 * 1024 * 1024)
|
|
736
|
+
throw new Error("Sitemap too large");
|
|
737
|
+
let stream = response.body;
|
|
738
|
+
const reader = stream.getReader();
|
|
739
|
+
const first = await reader.read();
|
|
740
|
+
const gzip = first.value?.[0] === 31 && first.value?.[1] === 139;
|
|
741
|
+
stream = new ReadableStream({
|
|
742
|
+
start(ctrl) {
|
|
743
|
+
if (first.value) ctrl.enqueue(first.value);
|
|
744
|
+
if (first.done) ctrl.close();
|
|
745
|
+
},
|
|
746
|
+
async pull(ctrl) {
|
|
747
|
+
const value = await reader.read();
|
|
748
|
+
value.done ? ctrl.close() : ctrl.enqueue(value.value);
|
|
749
|
+
},
|
|
750
|
+
cancel(reason) {
|
|
751
|
+
return reader.cancel(reason);
|
|
512
752
|
}
|
|
513
|
-
|
|
514
|
-
|
|
753
|
+
});
|
|
754
|
+
if (gzip) stream = stream.pipeThrough(new DecompressionStream("gzip"));
|
|
755
|
+
const decoded = stream.getReader(), chunks = [];
|
|
756
|
+
let size = 0;
|
|
757
|
+
try {
|
|
758
|
+
while (true) {
|
|
759
|
+
const next = await decoded.read();
|
|
760
|
+
if (next.done) break;
|
|
761
|
+
size += next.value.length;
|
|
762
|
+
if (size > 2 * 1024 * 1024) throw new Error("Sitemap too large");
|
|
763
|
+
chunks.push(next.value);
|
|
515
764
|
}
|
|
765
|
+
} finally {
|
|
766
|
+
await decoded.cancel().catch(() => {
|
|
767
|
+
});
|
|
516
768
|
}
|
|
769
|
+
return await new Blob(chunks).text();
|
|
517
770
|
} catch {
|
|
771
|
+
incomplete = true;
|
|
772
|
+
return null;
|
|
773
|
+
} finally {
|
|
774
|
+
clearTimeout(timer);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const robots = await read(new URL("/robots.txt", origin).href);
|
|
778
|
+
for (const match of (robots || "").matchAll(/^sitemap:\s*(\S+)/gim)) {
|
|
779
|
+
const url = sameOrigin(match[1]);
|
|
780
|
+
if (url) queue.push(url);
|
|
781
|
+
}
|
|
782
|
+
while (queue.length && visited.size < 8 && urls.length < 500 && !scan.cancelled && Date.now() < deadline) {
|
|
783
|
+
const url = queue.shift();
|
|
784
|
+
if (visited.has(url)) continue;
|
|
785
|
+
visited.add(url);
|
|
786
|
+
const content = await read(url);
|
|
787
|
+
if (!content) continue;
|
|
788
|
+
const xml = new DOMParser().parseFromString(content, "application/xml");
|
|
789
|
+
if (xml.querySelector("parsererror")) {
|
|
790
|
+
incomplete = true;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const index = xml.documentElement.localName === "sitemapindex";
|
|
794
|
+
for (const loc of xml.querySelectorAll("loc")) {
|
|
795
|
+
const value = sameOrigin(loc.textContent?.trim());
|
|
796
|
+
if (!value) continue;
|
|
797
|
+
if (index) {
|
|
798
|
+
if (!visited.has(value)) queue.push(value);
|
|
799
|
+
} else if (add(value)) sitemapCount++;
|
|
518
800
|
}
|
|
519
801
|
}
|
|
520
|
-
|
|
802
|
+
if (queue.length) incomplete = true;
|
|
803
|
+
if (globalThis.__zuckerExtScan === scan) delete globalThis.__zuckerExtScan;
|
|
804
|
+
return { urls, sitemapCount, incomplete, cancelled: scan.cancelled };
|
|
521
805
|
}
|
|
522
806
|
|
|
523
807
|
// src/popup.entry.ts
|