@zucker-framework/ext-kit 1.0.2 → 1.0.6
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/index.js
CHANGED
|
@@ -21,47 +21,89 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
var src_exports = {};
|
|
23
23
|
__export(src_exports, {
|
|
24
|
+
CAPTURE_LIMITS: () => CAPTURE_LIMITS,
|
|
25
|
+
assertCaptureFits: () => assertCaptureFits,
|
|
24
26
|
buildBatchFileName: () => buildBatchFileName,
|
|
25
27
|
buildQuickFileName: () => buildQuickFileName,
|
|
28
|
+
captureBudget: () => captureBudget,
|
|
29
|
+
captureFits: () => captureFits,
|
|
30
|
+
collectSameOriginLinks: () => collectSameOriginLinks,
|
|
31
|
+
createDownloader: () => createDownloader,
|
|
26
32
|
createLicensing: () => createLicensing,
|
|
27
33
|
fileStamp: () => fileStamp,
|
|
28
34
|
folderNameFor: () => folderNameFor,
|
|
35
|
+
hidePageCaptureDecorations: () => hidePageCaptureDecorations,
|
|
29
36
|
hostOf: () => hostOf,
|
|
30
37
|
initExtension: () => initExtension,
|
|
31
38
|
initPopup: () => initPopup,
|
|
32
39
|
normalizeUrls: () => normalizeUrls,
|
|
40
|
+
readDevicePixelRatio: () => readDevicePixelRatio,
|
|
33
41
|
sanitize: () => sanitize,
|
|
42
|
+
scrollCapture: () => scroll_capture_exports,
|
|
34
43
|
sessionStamp: () => sessionStamp,
|
|
44
|
+
showPageCaptureDecorations: () => showPageCaptureDecorations,
|
|
35
45
|
todayString: () => todayString,
|
|
36
46
|
urlBaseName: () => urlBaseName
|
|
37
47
|
});
|
|
38
48
|
module.exports = __toCommonJS(src_exports);
|
|
39
49
|
|
|
40
|
-
// src/util.
|
|
41
|
-
function
|
|
42
|
-
return new
|
|
50
|
+
// src/util.js
|
|
51
|
+
function cancelled() {
|
|
52
|
+
return new Error("Cancelled");
|
|
43
53
|
}
|
|
44
|
-
__name(
|
|
45
|
-
function
|
|
46
|
-
|
|
54
|
+
__name(cancelled, "cancelled");
|
|
55
|
+
function throwIfAborted(signal) {
|
|
56
|
+
if (signal?.aborted) throw cancelled();
|
|
57
|
+
}
|
|
58
|
+
__name(throwIfAborted, "throwIfAborted");
|
|
59
|
+
function withAbort(promise, signal) {
|
|
60
|
+
if (!signal) return promise;
|
|
61
|
+
if (signal.aborted) {
|
|
62
|
+
promise.catch(() => {
|
|
63
|
+
});
|
|
64
|
+
return Promise.reject(cancelled());
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const abort = /* @__PURE__ */ __name(() => reject(cancelled()), "abort");
|
|
68
|
+
signal.addEventListener("abort", abort, {
|
|
69
|
+
once: true
|
|
70
|
+
});
|
|
71
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
__name(withAbort, "withAbort");
|
|
75
|
+
function delay(ms, signal) {
|
|
76
|
+
throwIfAborted(signal);
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const finish = /* @__PURE__ */ __name(() => {
|
|
79
|
+
signal?.removeEventListener("abort", abort);
|
|
80
|
+
resolve();
|
|
81
|
+
}, "finish");
|
|
82
|
+
const timer = setTimeout(finish, ms);
|
|
83
|
+
const abort = /* @__PURE__ */ __name(() => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
reject(cancelled());
|
|
86
|
+
}, "abort");
|
|
87
|
+
signal?.addEventListener("abort", abort, {
|
|
88
|
+
once: true
|
|
89
|
+
});
|
|
90
|
+
});
|
|
47
91
|
}
|
|
48
|
-
__name(
|
|
92
|
+
__name(delay, "delay");
|
|
49
93
|
function normalizeError(error) {
|
|
50
94
|
return error instanceof Error ? error.message : String(error);
|
|
51
95
|
}
|
|
52
96
|
__name(normalizeError, "normalizeError");
|
|
53
|
-
function
|
|
54
|
-
return new Promise((resolve) => chrome.storage
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return new Promise((resolve) => chrome.storage.local.set(value, () => resolve()));
|
|
97
|
+
function storageCall(area, method, value) {
|
|
98
|
+
return new Promise((resolve, reject) => chrome.storage[area][method](value, (result) => {
|
|
99
|
+
const error = chrome.runtime.lastError;
|
|
100
|
+
error ? reject(new Error(error.message)) : resolve(result);
|
|
101
|
+
}));
|
|
59
102
|
}
|
|
60
|
-
__name(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
__name(storageRemove, "storageRemove");
|
|
103
|
+
__name(storageCall, "storageCall");
|
|
104
|
+
var storageGet = /* @__PURE__ */ __name((keys) => storageCall("local", "get", keys), "storageGet");
|
|
105
|
+
var storageSet = /* @__PURE__ */ __name((value) => storageCall("local", "set", value), "storageSet");
|
|
106
|
+
var storageRemove = /* @__PURE__ */ __name((keys) => storageCall("local", "remove", keys), "storageRemove");
|
|
65
107
|
|
|
66
108
|
// src/naming.ts
|
|
67
109
|
function sanitize(value) {
|
|
@@ -79,7 +121,7 @@ __name(hostOf, "hostOf");
|
|
|
79
121
|
function normalizeUrls(urls) {
|
|
80
122
|
const out = [];
|
|
81
123
|
const seen = /* @__PURE__ */ new Set();
|
|
82
|
-
for (const raw of urls
|
|
124
|
+
for (const raw of Array.isArray(urls) ? urls : []) {
|
|
83
125
|
const value = String(raw || "").trim();
|
|
84
126
|
if (!/^https?:\/\//i.test(value)) {
|
|
85
127
|
continue;
|
|
@@ -87,7 +129,6 @@ function normalizeUrls(urls) {
|
|
|
87
129
|
let key = value;
|
|
88
130
|
try {
|
|
89
131
|
const parsed = new URL(value);
|
|
90
|
-
parsed.hash = "";
|
|
91
132
|
key = parsed.toString();
|
|
92
133
|
} catch {
|
|
93
134
|
continue;
|
|
@@ -167,25 +208,67 @@ function buildQuickFileName(brand, quickFolder, pageUrl, extension) {
|
|
|
167
208
|
}
|
|
168
209
|
__name(buildQuickFileName, "buildQuickFileName");
|
|
169
210
|
|
|
170
|
-
// src/licensing.
|
|
171
|
-
var CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
172
|
-
function unwrap(payload) {
|
|
173
|
-
const obj = payload;
|
|
174
|
-
return obj && typeof obj === "object" && "result" in obj ? obj.result : obj || {};
|
|
175
|
-
}
|
|
176
|
-
__name(unwrap, "unwrap");
|
|
211
|
+
// src/licensing.js
|
|
177
212
|
function createLicensing(product, brand, config) {
|
|
213
|
+
for (const key of [
|
|
214
|
+
"cacheTtlMs",
|
|
215
|
+
"offlineGraceMs",
|
|
216
|
+
"requestTimeoutMs",
|
|
217
|
+
"retryDelayMs"
|
|
218
|
+
]) {
|
|
219
|
+
if (!Number.isFinite(config[key]) || config[key] < 0 || key === "requestTimeoutMs" && !config[key]) throw new RangeError(`Invalid licensing ${key}`);
|
|
220
|
+
}
|
|
178
221
|
const TOKEN_KEY = `${product}_license_token`;
|
|
179
222
|
const ENTITLEMENT_KEY = `${product}_entitlement`;
|
|
180
223
|
const DEVICE_KEY = `${product}_device_id`;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
224
|
+
let refresh = null;
|
|
225
|
+
let nextRefreshAt = 0;
|
|
226
|
+
let generation = 0;
|
|
227
|
+
let mutation = false;
|
|
228
|
+
let lastStatus = null;
|
|
229
|
+
const unavailable = /* @__PURE__ */ __name(() => ({
|
|
230
|
+
state: "unavailable",
|
|
231
|
+
message: "Could not verify your saved Pro license. Check your connection and use Recheck Pro in License. Your activation is still saved."
|
|
232
|
+
}), "unavailable");
|
|
233
|
+
const proStatus = /* @__PURE__ */ __name((entitlement) => ({
|
|
234
|
+
state: "pro",
|
|
235
|
+
entitlement
|
|
236
|
+
}), "proStatus");
|
|
237
|
+
function fallbackStatus(cached) {
|
|
238
|
+
if (lastStatus?.state === "invalid") return lastStatus;
|
|
239
|
+
const cachedEntitlement = usableCache(cached);
|
|
240
|
+
return cachedEntitlement ? proStatus(cachedEntitlement) : unavailable();
|
|
241
|
+
}
|
|
242
|
+
__name(fallbackStatus, "fallbackStatus");
|
|
243
|
+
function normalize(value) {
|
|
244
|
+
if (!value || value.invalid === true) return null;
|
|
245
|
+
const normalized = config.normalizeEntitlement(value);
|
|
246
|
+
if (!normalized || typeof normalized.expiresAt !== "string") return null;
|
|
247
|
+
const expires = Date.parse(normalized.expiresAt);
|
|
248
|
+
return Number.isFinite(expires) && expires > Date.now() ? normalized : null;
|
|
249
|
+
}
|
|
250
|
+
__name(normalize, "normalize");
|
|
251
|
+
function usableCache(cached) {
|
|
252
|
+
const age = Date.now() - Number(cached?.checkedAt);
|
|
253
|
+
return Number.isFinite(age) && age >= 0 && age < config.offlineGraceMs ? normalize(cached) : null;
|
|
254
|
+
}
|
|
255
|
+
__name(usableCache, "usableCache");
|
|
256
|
+
async function request(operation, input = {}) {
|
|
257
|
+
const controller = new AbortController();
|
|
258
|
+
const timer = setTimeout(() => controller.abort(), config.requestTimeoutMs);
|
|
259
|
+
try {
|
|
260
|
+
return await withAbort(Promise.resolve().then(() => config.request(operation, input, controller.signal)), controller.signal);
|
|
261
|
+
} catch {
|
|
262
|
+
throw new Error("License service unreachable. Check your connection and retry.");
|
|
263
|
+
} finally {
|
|
264
|
+
clearTimeout(timer);
|
|
185
265
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
266
|
+
}
|
|
267
|
+
__name(request, "request");
|
|
268
|
+
async function getDeviceId() {
|
|
269
|
+
const stored = await storageGet(DEVICE_KEY);
|
|
270
|
+
if (stored[DEVICE_KEY]) return stored[DEVICE_KEY];
|
|
271
|
+
const id = crypto.randomUUID();
|
|
189
272
|
await storageSet({
|
|
190
273
|
[DEVICE_KEY]: id
|
|
191
274
|
});
|
|
@@ -193,256 +276,730 @@ function createLicensing(product, brand, config) {
|
|
|
193
276
|
}
|
|
194
277
|
__name(getDeviceId, "getDeviceId");
|
|
195
278
|
async function activate(code) {
|
|
196
|
-
|
|
197
|
-
|
|
279
|
+
if (mutation) throw new Error("A license change is already in progress");
|
|
280
|
+
mutation = true;
|
|
281
|
+
generation += 1;
|
|
198
282
|
try {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
},
|
|
205
|
-
body: JSON.stringify({
|
|
206
|
-
code: String(code || "").trim().toUpperCase().replace(/[^A-Z0-9]/g, ""),
|
|
207
|
-
deviceId,
|
|
208
|
-
deviceName: `${brand} \xB7 ${platform}`
|
|
209
|
-
})
|
|
283
|
+
const deviceId = await getDeviceId();
|
|
284
|
+
const data = await request("activate", {
|
|
285
|
+
code,
|
|
286
|
+
deviceId,
|
|
287
|
+
brand
|
|
210
288
|
});
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
289
|
+
if (!data.accepted) throw new Error(data.denied ? "Invalid or expired code, or its device limit is reached." : "Activation failed. Please retry later.");
|
|
290
|
+
const entitlement = normalize(data?.entitlement);
|
|
291
|
+
if (!entitlement || typeof data?.token !== "string" || !data.token) throw new Error("License service returned invalid activation data. Please retry.");
|
|
292
|
+
await storageSet({
|
|
293
|
+
[TOKEN_KEY]: data.token,
|
|
294
|
+
[ENTITLEMENT_KEY]: {
|
|
295
|
+
...entitlement,
|
|
296
|
+
checkedAt: Date.now()
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
nextRefreshAt = 0;
|
|
300
|
+
lastStatus = proStatus(entitlement);
|
|
301
|
+
return entitlement;
|
|
302
|
+
} finally {
|
|
303
|
+
mutation = false;
|
|
220
304
|
}
|
|
221
|
-
const data = unwrap(payload);
|
|
222
|
-
await storageSet({
|
|
223
|
-
[TOKEN_KEY]: data.token || "",
|
|
224
|
-
[ENTITLEMENT_KEY]: {
|
|
225
|
-
...data.entitlement || {},
|
|
226
|
-
checkedAt: Date.now()
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
return data.entitlement;
|
|
230
305
|
}
|
|
231
306
|
__name(activate, "activate");
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
307
|
+
async function refreshEntitlement(token, cached, version) {
|
|
308
|
+
try {
|
|
309
|
+
const data = await request("me", {
|
|
310
|
+
token
|
|
311
|
+
});
|
|
312
|
+
if (version !== generation) return null;
|
|
313
|
+
if (data.denied === true) {
|
|
314
|
+
const denied = {
|
|
315
|
+
state: "invalid",
|
|
316
|
+
message: "Your saved Pro license could not be validated. Use Recheck Pro or enter your original activation code in License to restore it."
|
|
317
|
+
};
|
|
318
|
+
lastStatus = denied;
|
|
319
|
+
try {
|
|
320
|
+
await storageRemove(ENTITLEMENT_KEY);
|
|
321
|
+
} catch {
|
|
322
|
+
await storageSet({
|
|
323
|
+
[ENTITLEMENT_KEY]: {
|
|
324
|
+
invalid: true,
|
|
325
|
+
active: false,
|
|
326
|
+
checkedAt: Date.now()
|
|
327
|
+
}
|
|
328
|
+
}).catch(() => {
|
|
329
|
+
console.warn(`${brand}: rejected license cache could not be invalidated`);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return denied;
|
|
333
|
+
}
|
|
334
|
+
const entitlement = data.accepted ? normalize(data.entitlement) : null;
|
|
335
|
+
if (entitlement) {
|
|
336
|
+
await storageSet({
|
|
337
|
+
[ENTITLEMENT_KEY]: {
|
|
338
|
+
...entitlement,
|
|
339
|
+
checkedAt: Date.now()
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return proStatus(entitlement);
|
|
343
|
+
}
|
|
344
|
+
return fallbackStatus(cached);
|
|
345
|
+
} catch {
|
|
346
|
+
if (version !== generation) return null;
|
|
347
|
+
return fallbackStatus(cached);
|
|
236
348
|
}
|
|
237
|
-
const expired = v.expiresAt && new Date(v.expiresAt).getTime() <= Date.now();
|
|
238
|
-
return expired ? null : {
|
|
239
|
-
plan: v.plan || "pro",
|
|
240
|
-
expiresAt: v.expiresAt || ""
|
|
241
|
-
};
|
|
242
349
|
}
|
|
243
|
-
__name(
|
|
244
|
-
async function
|
|
350
|
+
__name(refreshEntitlement, "refreshEntitlement");
|
|
351
|
+
async function getStatus({ cachedOnly = false, force = false } = {}) {
|
|
245
352
|
const data = await storageGet([
|
|
246
353
|
TOKEN_KEY,
|
|
247
354
|
ENTITLEMENT_KEY
|
|
248
355
|
]);
|
|
249
|
-
const token = data[TOKEN_KEY];
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
356
|
+
const token = data[TOKEN_KEY], cached = data[ENTITLEMENT_KEY];
|
|
357
|
+
if (!token) return {
|
|
358
|
+
state: "free"
|
|
359
|
+
};
|
|
360
|
+
const valid = lastStatus?.state === "invalid" ? null : usableCache(cached);
|
|
361
|
+
if (!force && valid && Date.now() - cached.checkedAt < config.cacheTtlMs) return proStatus(valid);
|
|
362
|
+
if (mutation) return {
|
|
363
|
+
state: "checking",
|
|
364
|
+
refreshing: true
|
|
365
|
+
};
|
|
366
|
+
if (!refresh && (force || Date.now() >= nextRefreshAt)) {
|
|
367
|
+
const version = generation;
|
|
368
|
+
nextRefreshAt = Date.now() + config.retryDelayMs;
|
|
369
|
+
refresh = refreshEntitlement(token, cached, version).then((status2) => {
|
|
370
|
+
if (version === generation && status2) lastStatus = status2;
|
|
371
|
+
return status2;
|
|
372
|
+
}).finally(() => {
|
|
373
|
+
refresh = null;
|
|
263
374
|
});
|
|
264
|
-
} catch {
|
|
265
|
-
return cached ? normalize(cached) : null;
|
|
266
375
|
}
|
|
267
|
-
if (!
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
return cached ? normalize(cached) : null;
|
|
376
|
+
if (refresh && !cachedOnly) {
|
|
377
|
+
const status2 = await refresh;
|
|
378
|
+
return status2 || getStatus();
|
|
273
379
|
}
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
return normalize(entitlement);
|
|
380
|
+
const remembered = lastStatus?.state === "pro" ? null : lastStatus;
|
|
381
|
+
const status = valid ? proStatus(valid) : refresh ? {
|
|
382
|
+
state: "checking"
|
|
383
|
+
} : remembered || unavailable();
|
|
384
|
+
return {
|
|
385
|
+
...status,
|
|
386
|
+
refreshing: Boolean(refresh)
|
|
387
|
+
};
|
|
283
388
|
}
|
|
284
|
-
__name(
|
|
285
|
-
async function
|
|
286
|
-
return
|
|
389
|
+
__name(getStatus, "getStatus");
|
|
390
|
+
async function getEntitlement(options) {
|
|
391
|
+
return (await getStatus(options)).entitlement || null;
|
|
287
392
|
}
|
|
288
|
-
__name(
|
|
289
|
-
async function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
393
|
+
__name(getEntitlement, "getEntitlement");
|
|
394
|
+
async function deactivate() {
|
|
395
|
+
if (mutation) throw new Error("A license change is already in progress");
|
|
396
|
+
mutation = true;
|
|
397
|
+
generation += 1;
|
|
398
|
+
try {
|
|
399
|
+
const data = await storageGet(TOKEN_KEY);
|
|
400
|
+
if (data[TOKEN_KEY]) {
|
|
401
|
+
const result = await request("deactivate", {
|
|
402
|
+
token: data[TOKEN_KEY]
|
|
403
|
+
});
|
|
404
|
+
if (!result.accepted || result.deactivated !== true) throw new Error("Could not release this device. Your license is still saved; please retry.");
|
|
405
|
+
}
|
|
406
|
+
await storageRemove([
|
|
407
|
+
TOKEN_KEY,
|
|
408
|
+
ENTITLEMENT_KEY
|
|
409
|
+
]);
|
|
410
|
+
nextRefreshAt = 0;
|
|
411
|
+
lastStatus = null;
|
|
412
|
+
} finally {
|
|
413
|
+
mutation = false;
|
|
414
|
+
}
|
|
294
415
|
}
|
|
295
|
-
__name(
|
|
416
|
+
__name(deactivate, "deactivate");
|
|
296
417
|
return {
|
|
297
418
|
activate,
|
|
298
419
|
getEntitlement,
|
|
299
|
-
|
|
300
|
-
|
|
420
|
+
getStatus,
|
|
421
|
+
deactivate,
|
|
301
422
|
purchaseUrl: config.purchaseUrl
|
|
302
423
|
};
|
|
303
424
|
}
|
|
304
425
|
__name(createLicensing, "createLicensing");
|
|
305
426
|
|
|
306
|
-
// src/download.
|
|
307
|
-
function createDownloader(justification) {
|
|
308
|
-
const
|
|
427
|
+
// src/download.js
|
|
428
|
+
function createDownloader(justification, options = {}) {
|
|
429
|
+
const deadlineMs = options.deadlineMs ?? 12e4;
|
|
430
|
+
const blobLeaseMs = options.blobLeaseMs ?? 18e4;
|
|
431
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
432
|
+
if (![
|
|
433
|
+
deadlineMs,
|
|
434
|
+
blobLeaseMs,
|
|
435
|
+
pollMs
|
|
436
|
+
].every((n) => Number.isFinite(n) && n > 0) || blobLeaseMs <= deadlineMs) throw new RangeError("Invalid download deadline, polling or blob lease");
|
|
437
|
+
const names = /* @__PURE__ */ new Map();
|
|
438
|
+
const waiters = /* @__PURE__ */ new Map();
|
|
309
439
|
let offscreenSetup = null;
|
|
310
440
|
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
suggest({
|
|
316
|
-
filename: fileName,
|
|
441
|
+
const filename = names.get(item.url) || names.get(item.finalUrl);
|
|
442
|
+
if (filename) suggest({
|
|
443
|
+
filename,
|
|
317
444
|
conflictAction: "uniquify"
|
|
318
445
|
});
|
|
319
|
-
|
|
446
|
+
});
|
|
447
|
+
chrome.downloads.onChanged.addListener((delta) => {
|
|
448
|
+
if (delta.state?.current === "complete" || delta.state?.current === "interrupted") waiters.get(delta.id)?.();
|
|
320
449
|
});
|
|
321
450
|
async function ensureOffscreen() {
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (!offscreenSetup) {
|
|
326
|
-
offscreenSetup = chrome.offscreen.createDocument({
|
|
327
|
-
url: "offscreen.html",
|
|
451
|
+
if (!offscreenSetup) offscreenSetup = (async () => {
|
|
452
|
+
if (!await chrome.offscreen.hasDocument()) await chrome.offscreen.createDocument({
|
|
453
|
+
url: options.offscreenUrl || "offscreen.html",
|
|
328
454
|
reasons: [
|
|
329
455
|
chrome.offscreen.Reason.BLOBS
|
|
330
456
|
],
|
|
331
457
|
justification
|
|
332
|
-
}).catch((error) => {
|
|
333
|
-
if (!String(error?.message || "").includes("Only a single")) {
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
}).finally(() => {
|
|
337
|
-
offscreenSetup = null;
|
|
338
458
|
});
|
|
339
|
-
}
|
|
459
|
+
})().finally(() => {
|
|
460
|
+
offscreenSetup = null;
|
|
461
|
+
});
|
|
340
462
|
return offscreenSetup;
|
|
341
463
|
}
|
|
342
464
|
__name(ensureOffscreen, "ensureOffscreen");
|
|
343
|
-
async function
|
|
465
|
+
async function cleanup(url) {
|
|
466
|
+
names.delete(url);
|
|
467
|
+
if (url) await chrome.runtime.sendMessage({
|
|
468
|
+
target: "offscreen",
|
|
469
|
+
type: "REVOKE_BLOB_URL",
|
|
470
|
+
url
|
|
471
|
+
}).catch(() => {
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
__name(cleanup, "cleanup");
|
|
475
|
+
async function waitForDownload(id, signal) {
|
|
476
|
+
return new Promise((resolve, reject) => {
|
|
477
|
+
let finished = false;
|
|
478
|
+
let deadlineError;
|
|
479
|
+
const finish = /* @__PURE__ */ __name((error, item) => {
|
|
480
|
+
if (finished) return;
|
|
481
|
+
finished = true;
|
|
482
|
+
clearInterval(poll);
|
|
483
|
+
clearTimeout(timeout);
|
|
484
|
+
signal?.removeEventListener("abort", abort);
|
|
485
|
+
waiters.delete(id);
|
|
486
|
+
error ? reject(error) : resolve(item);
|
|
487
|
+
}, "finish");
|
|
488
|
+
const inspect = /* @__PURE__ */ __name(async () => {
|
|
489
|
+
try {
|
|
490
|
+
const [item] = await chrome.downloads.search({
|
|
491
|
+
id
|
|
492
|
+
});
|
|
493
|
+
if (!item) return finish(new Error("Download no longer exists"));
|
|
494
|
+
if (item.state === "complete") finish(null, item);
|
|
495
|
+
else if (item.state === "interrupted") finish(deadlineError || new Error(item.error === "USER_CANCELED" ? "Cancelled" : `Download interrupted: ${item.error || "unknown error"}`));
|
|
496
|
+
} catch (error) {
|
|
497
|
+
finish(error);
|
|
498
|
+
}
|
|
499
|
+
}, "inspect");
|
|
500
|
+
const abort = /* @__PURE__ */ __name(async () => {
|
|
501
|
+
await chrome.downloads.cancel(id).catch(() => {
|
|
502
|
+
});
|
|
503
|
+
const [item] = await chrome.downloads.search({
|
|
504
|
+
id
|
|
505
|
+
}).catch(() => []);
|
|
506
|
+
item?.state === "complete" ? finish(null, item) : finish(cancelled());
|
|
507
|
+
}, "abort");
|
|
508
|
+
const poll = setInterval(inspect, pollMs);
|
|
509
|
+
const timeout = setTimeout(async () => {
|
|
510
|
+
deadlineError = new Error("Download completion timed out");
|
|
511
|
+
await chrome.downloads.cancel(id).catch(() => {
|
|
512
|
+
});
|
|
513
|
+
finish(deadlineError);
|
|
514
|
+
}, deadlineMs);
|
|
515
|
+
waiters.set(id, inspect);
|
|
516
|
+
signal?.addEventListener("abort", abort, {
|
|
517
|
+
once: true
|
|
518
|
+
});
|
|
519
|
+
if (signal?.aborted) void abort();
|
|
520
|
+
else void inspect();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
__name(waitForDownload, "waitForDownload");
|
|
524
|
+
async function recover(item, signal) {
|
|
525
|
+
const matches = item.downloadId != null ? await chrome.downloads.search({
|
|
526
|
+
id: item.downloadId
|
|
527
|
+
}) : await chrome.downloads.search({
|
|
528
|
+
url: `^${item.downloadUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`
|
|
529
|
+
});
|
|
530
|
+
const download = matches.find((d) => d.byExtensionId === chrome.runtime.id && (!item.downloadUrl || d.url === item.downloadUrl));
|
|
531
|
+
if (!download) {
|
|
532
|
+
await cleanup(item.downloadUrl);
|
|
533
|
+
throw new Error("Interrupted download could not be confirmed. Retry this page.");
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
return await waitForDownload(download.id, signal);
|
|
537
|
+
} finally {
|
|
538
|
+
await cleanup(item.downloadUrl);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
__name(recover, "recover");
|
|
542
|
+
async function downloadViaBlob(base64, mime, filename, { signal, onPrepared, onCreated } = {}) {
|
|
543
|
+
throwIfAborted(signal);
|
|
344
544
|
await ensureOffscreen();
|
|
545
|
+
throwIfAborted(signal);
|
|
345
546
|
const created = await chrome.runtime.sendMessage({
|
|
346
547
|
target: "offscreen",
|
|
347
548
|
type: "CREATE_BLOB_URL",
|
|
348
549
|
base64,
|
|
349
|
-
mime
|
|
550
|
+
mime,
|
|
551
|
+
leaseMs: blobLeaseMs
|
|
350
552
|
});
|
|
351
|
-
if (!created?.ok || !created.url)
|
|
352
|
-
throw new Error(created?.error || "Failed to prepare download");
|
|
353
|
-
}
|
|
553
|
+
if (!created?.ok || !created.url) throw new Error(created?.error || "Failed to prepare download");
|
|
354
554
|
const url = created.url;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
pendingDownloadNames.delete(url);
|
|
358
|
-
chrome.runtime.sendMessage({
|
|
359
|
-
target: "offscreen",
|
|
360
|
-
type: "REVOKE_BLOB_URL",
|
|
361
|
-
url
|
|
362
|
-
}).catch(() => {
|
|
363
|
-
});
|
|
364
|
-
}, "cleanup");
|
|
555
|
+
names.set(url, filename);
|
|
556
|
+
let downloadId;
|
|
365
557
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const error = chrome.runtime.lastError;
|
|
374
|
-
error ? reject(new Error(error.message)) : resolve(id);
|
|
375
|
-
});
|
|
558
|
+
await onPrepared?.(url);
|
|
559
|
+
throwIfAborted(signal);
|
|
560
|
+
downloadId = await chrome.downloads.download({
|
|
561
|
+
url,
|
|
562
|
+
filename,
|
|
563
|
+
conflictAction: "uniquify",
|
|
564
|
+
saveAs: false
|
|
376
565
|
});
|
|
377
|
-
|
|
566
|
+
await onCreated?.(downloadId);
|
|
567
|
+
await waitForDownload(downloadId, signal);
|
|
378
568
|
return downloadId;
|
|
379
569
|
} catch (error) {
|
|
380
|
-
|
|
570
|
+
if (downloadId != null) await chrome.downloads.cancel(downloadId).catch(() => {
|
|
571
|
+
});
|
|
381
572
|
throw error;
|
|
573
|
+
} finally {
|
|
574
|
+
await cleanup(url);
|
|
382
575
|
}
|
|
383
576
|
}
|
|
384
577
|
__name(downloadViaBlob, "downloadViaBlob");
|
|
385
578
|
return {
|
|
386
|
-
downloadViaBlob
|
|
579
|
+
downloadViaBlob,
|
|
580
|
+
recover
|
|
387
581
|
};
|
|
388
582
|
}
|
|
389
583
|
__name(createDownloader, "createDownloader");
|
|
390
584
|
|
|
391
|
-
// src/
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
585
|
+
// src/scroll-capture.js
|
|
586
|
+
var scroll_capture_exports = {};
|
|
587
|
+
__export(scroll_capture_exports, {
|
|
588
|
+
prepare: () => prepare,
|
|
589
|
+
restore: () => restore
|
|
590
|
+
});
|
|
591
|
+
var sessions = /* @__PURE__ */ new WeakMap();
|
|
592
|
+
function pageTask(action, id, index, leaseMs) {
|
|
593
|
+
const key = "__zuckerScrollCapture";
|
|
594
|
+
let state = window[key];
|
|
595
|
+
if (action === "restore") {
|
|
596
|
+
if (state?.id === id) state.restore();
|
|
597
|
+
return {};
|
|
598
|
+
}
|
|
599
|
+
if (action === "init") {
|
|
600
|
+
if (state) throw new Error("Another scrolling-content capture is active");
|
|
601
|
+
const elements = [
|
|
602
|
+
...document.querySelectorAll("*")
|
|
603
|
+
];
|
|
604
|
+
if (elements.length > 3e4) throw new Error("This page is too complex to expand its scrolling content safely.");
|
|
605
|
+
const panels = elements.filter((el) => {
|
|
606
|
+
if (el === document.body || el === document.documentElement || el === document.scrollingElement || el.closest('nav,aside,[role="navigation"],[role="menu"],textarea,select')) return false;
|
|
607
|
+
const rect = el.getBoundingClientRect(), css = getComputedStyle(el);
|
|
608
|
+
if (rect.width < 120 || rect.height < 80 || el.scrollHeight <= el.clientHeight + 2 || !/^(auto|scroll|overlay)$/.test(css.overflowY) || css.visibility !== "visible") return false;
|
|
609
|
+
for (let parent = el; parent; parent = parent.parentElement) {
|
|
610
|
+
const style = getComputedStyle(parent);
|
|
611
|
+
if (style.display === "none" || Number(style.opacity) === 0) return false;
|
|
612
|
+
}
|
|
613
|
+
return true;
|
|
397
614
|
});
|
|
615
|
+
if (!panels.length) return {
|
|
616
|
+
count: 0
|
|
617
|
+
};
|
|
618
|
+
if (panels.length > 24) throw new Error("This page has too many independent scrolling areas. Use Visible window or export smaller sections.");
|
|
619
|
+
const virtual = '.rc-virtual-list-holder,.ant-table-tbody-virtual-holder,[data-virtuoso-scroller],.ReactVirtualized__Grid,.ReactVirtualized__List,[data-virtualized="true"]';
|
|
620
|
+
if (panels.some((el) => el.matches(virtual) || [
|
|
621
|
+
...el.querySelectorAll(virtual)
|
|
622
|
+
].some((list) => list.getClientRects().length && list.checkVisibility({
|
|
623
|
+
checkOpacity: true,
|
|
624
|
+
checkVisibilityCSS: true
|
|
625
|
+
})))) throw new Error("This page uses a virtual scrolling list. A complete full-page capture is not available; use the table's export or Visible window.");
|
|
626
|
+
const changes = /* @__PURE__ */ new Map(), scrolls = /* @__PURE__ */ new Map();
|
|
627
|
+
state = {
|
|
628
|
+
id,
|
|
629
|
+
panels,
|
|
630
|
+
changes,
|
|
631
|
+
scrolls,
|
|
632
|
+
boxes: /* @__PURE__ */ new Map(),
|
|
633
|
+
watchdog: null,
|
|
634
|
+
leaseMs
|
|
635
|
+
};
|
|
636
|
+
state.set = (el, property, value) => {
|
|
637
|
+
if (!changes.has(el)) changes.set(el, {
|
|
638
|
+
hadStyle: el.hasAttribute("style"),
|
|
639
|
+
properties: /* @__PURE__ */ new Map()
|
|
640
|
+
});
|
|
641
|
+
const saved = changes.get(el).properties;
|
|
642
|
+
if (!saved.has(property)) saved.set(property, {
|
|
643
|
+
value: el.style.getPropertyValue(property),
|
|
644
|
+
priority: el.style.getPropertyPriority(property)
|
|
645
|
+
});
|
|
646
|
+
el.style.setProperty(property, value, "important");
|
|
647
|
+
};
|
|
648
|
+
state.restore = () => {
|
|
649
|
+
clearTimeout(state.watchdog);
|
|
650
|
+
const scrollingProperties = /* @__PURE__ */ new Set([
|
|
651
|
+
"scroll-behavior",
|
|
652
|
+
"scroll-snap-type",
|
|
653
|
+
"overflow-anchor"
|
|
654
|
+
]);
|
|
655
|
+
const restoreProperty = /* @__PURE__ */ __name((el, property, old) => old.value ? el.style.setProperty(property, old.value, old.priority) : el.style.removeProperty(property), "restoreProperty");
|
|
656
|
+
for (const [el, saved] of [
|
|
657
|
+
...changes
|
|
658
|
+
].reverse()) for (const [property, old] of saved.properties) if (!scrollingProperties.has(property)) restoreProperty(el, property, old);
|
|
659
|
+
for (const [el, saved] of scrolls) {
|
|
660
|
+
el.scrollLeft = saved.x;
|
|
661
|
+
el.scrollTop = saved.y;
|
|
662
|
+
}
|
|
663
|
+
for (const [el, saved] of changes) {
|
|
664
|
+
for (const [property, old] of saved.properties) if (scrollingProperties.has(property)) restoreProperty(el, property, old);
|
|
665
|
+
if (!saved.hadStyle && !el.style.length) {
|
|
666
|
+
el.getAttribute("style");
|
|
667
|
+
el.removeAttribute("style");
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (window[key]?.id === id) delete window[key];
|
|
671
|
+
};
|
|
672
|
+
window[key] = state;
|
|
673
|
+
state.watchdog = setTimeout(state.restore, state.leaseMs);
|
|
674
|
+
for (const el of elements) {
|
|
675
|
+
if (!el.scrollTop && !el.scrollLeft) continue;
|
|
676
|
+
scrolls.set(el, {
|
|
677
|
+
x: el.scrollLeft,
|
|
678
|
+
y: el.scrollTop
|
|
679
|
+
});
|
|
680
|
+
state.set(el, "scroll-behavior", "auto");
|
|
681
|
+
state.set(el, "scroll-snap-type", "none");
|
|
682
|
+
state.set(el, "overflow-anchor", "none");
|
|
683
|
+
}
|
|
684
|
+
for (const panel of panels) {
|
|
685
|
+
const rect = panel.getBoundingClientRect();
|
|
686
|
+
state.boxes.set(panel, {
|
|
687
|
+
width: rect.width,
|
|
688
|
+
x: rect.x
|
|
689
|
+
});
|
|
690
|
+
scrolls.set(panel, {
|
|
691
|
+
x: panel.scrollLeft,
|
|
692
|
+
y: panel.scrollTop
|
|
693
|
+
});
|
|
694
|
+
state.set(panel, "scroll-behavior", "auto");
|
|
695
|
+
state.set(panel, "scroll-snap-type", "none");
|
|
696
|
+
state.set(panel, "overflow-anchor", "none");
|
|
697
|
+
panel.scrollTop = 0;
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
count: panels.length
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (!state || state.id !== id) throw new Error("The page changed while preparing its scrolling content.");
|
|
704
|
+
clearTimeout(state.watchdog);
|
|
705
|
+
state.watchdog = setTimeout(state.restore, state.leaseMs);
|
|
706
|
+
if (action === "scroll" || action === "position") {
|
|
707
|
+
const panel = state.panels[index];
|
|
708
|
+
if (!panel.isConnected) throw new Error("A scrolling area changed during capture. Please retry.");
|
|
709
|
+
if (action === "scroll") panel.scrollTop += Math.max(80, panel.clientHeight * 0.8);
|
|
710
|
+
return {
|
|
711
|
+
bottom: panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 1,
|
|
712
|
+
height: panel.scrollHeight
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
if (action === "expand") {
|
|
716
|
+
for (const panel of [
|
|
717
|
+
...state.panels
|
|
718
|
+
].reverse()) {
|
|
719
|
+
if (!panel.isConnected) throw new Error("A scrolling area changed during capture. Please retry.");
|
|
720
|
+
let el = panel;
|
|
721
|
+
while (el) {
|
|
722
|
+
const rect = el.getBoundingClientRect(), css = getComputedStyle(el);
|
|
723
|
+
if (!state.scrolls.has(el)) state.scrolls.set(el, {
|
|
724
|
+
x: el.scrollLeft,
|
|
725
|
+
y: el.scrollTop
|
|
726
|
+
});
|
|
727
|
+
if (!state.boxes.has(el)) state.boxes.set(el, {
|
|
728
|
+
width: rect.width,
|
|
729
|
+
x: rect.x
|
|
730
|
+
});
|
|
731
|
+
state.set(el, "scroll-behavior", "auto");
|
|
732
|
+
state.set(el, "overflow-anchor", "none");
|
|
733
|
+
state.set(el, "transition", "none");
|
|
734
|
+
state.set(el, "box-sizing", "border-box");
|
|
735
|
+
state.set(el, "width", `${state.boxes.get(el).width}px`);
|
|
736
|
+
state.set(el, "max-height", "none");
|
|
737
|
+
state.set(el, "flex-shrink", "0");
|
|
738
|
+
if (css.position === "fixed") {
|
|
739
|
+
state.set(el, "position", "absolute");
|
|
740
|
+
state.set(el, "top", `${rect.top + scrollY}px`);
|
|
741
|
+
state.set(el, "left", `${rect.left + scrollX}px`);
|
|
742
|
+
state.set(el, "right", "auto");
|
|
743
|
+
}
|
|
744
|
+
if (css.position === "fixed" || css.position === "absolute") state.set(el, "bottom", "auto");
|
|
745
|
+
if (state.panels.includes(el)) {
|
|
746
|
+
const height = el.scrollHeight + el.offsetHeight - el.clientHeight;
|
|
747
|
+
state.set(el, "height", `${height}px`);
|
|
748
|
+
state.set(el, "min-height", `${height}px`);
|
|
749
|
+
state.set(el, "scrollbar-gutter", "stable");
|
|
750
|
+
state.set(el, "overflow-y", "hidden");
|
|
751
|
+
el.scrollTop = 0;
|
|
752
|
+
} else {
|
|
753
|
+
state.set(el, "height", "auto");
|
|
754
|
+
state.set(el, "min-height", `${Math.max(rect.height, el.clientHeight)}px`);
|
|
755
|
+
if (/hidden|clip|auto|scroll/.test(css.overflowY)) {
|
|
756
|
+
state.set(el, "overflow-x", "clip");
|
|
757
|
+
state.set(el, "overflow-y", "visible");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (css.contain !== "none") state.set(el, "contain", "none");
|
|
761
|
+
el = el.parentElement;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
height: document.documentElement.scrollHeight
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
if (action === "measure") {
|
|
769
|
+
for (const panel of state.panels) {
|
|
770
|
+
const rect = panel.getBoundingClientRect(), original = state.boxes.get(panel);
|
|
771
|
+
if (Math.abs(rect.width - original.width) > 1 || Math.abs(rect.x - original.x) > 1) throw new Error("This scrolling layout cannot be expanded without changing its width. Use Visible window or the website's export.");
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0),
|
|
775
|
+
panels: state.panels.map((el) => [
|
|
776
|
+
el.scrollHeight,
|
|
777
|
+
el.clientHeight
|
|
778
|
+
]),
|
|
779
|
+
ready: document.fonts.status === "loaded" && [
|
|
780
|
+
...document.images
|
|
781
|
+
].every((image) => image.complete || !state.panels.some((panel) => panel.contains(image)) || !image.getClientRects().length || image.checkVisibility?.({
|
|
782
|
+
checkOpacity: true,
|
|
783
|
+
checkVisibilityCSS: true
|
|
784
|
+
}) === false)
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return {};
|
|
788
|
+
}
|
|
789
|
+
__name(pageTask, "pageTask");
|
|
790
|
+
async function command(ctx, id, action, index, leaseMs) {
|
|
791
|
+
const response = await ctx[action === "restore" ? "cleanup" : "send"]("Runtime.evaluate", {
|
|
792
|
+
expression: `(${pageTask.toString()})(${JSON.stringify(action)},${JSON.stringify(id)},${JSON.stringify(index)},${JSON.stringify(leaseMs)})`,
|
|
793
|
+
returnByValue: true
|
|
398
794
|
});
|
|
795
|
+
if (response?.exceptionDetails) throw new Error(response.exceptionDetails.exception?.description?.split("\n")[0] || "Could not prepare scrolling content");
|
|
796
|
+
return response?.result?.value || {};
|
|
797
|
+
}
|
|
798
|
+
__name(command, "command");
|
|
799
|
+
async function prepare(ctx, waitForContent = async () => {
|
|
800
|
+
}) {
|
|
801
|
+
const id = crypto.randomUUID();
|
|
802
|
+
sessions.set(ctx, id);
|
|
803
|
+
const readinessMs = Math.min(6e4, Math.max(0, Number(ctx.options?.readyTimeoutMs) || 25e3));
|
|
804
|
+
const initial = await command(ctx, id, "init", null, Math.max(6e4, readinessMs + 15e3));
|
|
805
|
+
if (!initial.count) {
|
|
806
|
+
sessions.delete(ctx);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
let deadline = Date.now() + readinessMs + 15e3;
|
|
810
|
+
for (let index = 0; index < initial.count; index++) {
|
|
811
|
+
let bottom = false;
|
|
812
|
+
for (let step = 0; step < 60 && Date.now() < deadline; step++) {
|
|
813
|
+
const position = await command(ctx, id, "scroll", index);
|
|
814
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
815
|
+
if (position.bottom) {
|
|
816
|
+
await waitForContent();
|
|
817
|
+
const loaded = await command(ctx, id, "position", index);
|
|
818
|
+
if (loaded.bottom && loaded.height === position.height) {
|
|
819
|
+
bottom = true;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (!bottom) throw new Error("Scrolling content keeps growing or exceeds the capture limit. Use Visible window or a smaller section.");
|
|
825
|
+
}
|
|
826
|
+
await waitForContent();
|
|
827
|
+
await command(ctx, id, "expand");
|
|
828
|
+
await waitForContent();
|
|
829
|
+
deadline = Date.now() + 15e3;
|
|
830
|
+
let previous = "", stable = 0;
|
|
831
|
+
while (Date.now() < deadline) {
|
|
832
|
+
await command(ctx, id, "expand");
|
|
833
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
834
|
+
const measured = await command(ctx, id, "measure");
|
|
835
|
+
const signature = JSON.stringify([
|
|
836
|
+
measured.height,
|
|
837
|
+
measured.panels
|
|
838
|
+
]);
|
|
839
|
+
stable = measured.ready && signature === previous && measured.panels.every(([full, view]) => full <= view + 2) ? stable + 1 : 0;
|
|
840
|
+
if (stable >= 2) return;
|
|
841
|
+
previous = signature;
|
|
842
|
+
}
|
|
843
|
+
throw new Error("Scrolling content did not settle in time. Wait for it to finish loading and retry.");
|
|
844
|
+
}
|
|
845
|
+
__name(prepare, "prepare");
|
|
846
|
+
async function restore(ctx) {
|
|
847
|
+
const id = sessions.get(ctx);
|
|
848
|
+
if (!id) return;
|
|
849
|
+
sessions.delete(ctx);
|
|
850
|
+
await command(ctx, id, "restore");
|
|
851
|
+
}
|
|
852
|
+
__name(restore, "restore");
|
|
853
|
+
|
|
854
|
+
// src/session.js
|
|
855
|
+
function debuggerAttach(target, version) {
|
|
856
|
+
return new Promise((resolve, reject) => chrome.debugger.attach(target, version, () => {
|
|
857
|
+
const error = chrome.runtime.lastError;
|
|
858
|
+
error ? reject(new Error(error.message)) : resolve();
|
|
859
|
+
}));
|
|
399
860
|
}
|
|
400
861
|
__name(debuggerAttach, "debuggerAttach");
|
|
401
862
|
function debuggerDetach(target) {
|
|
402
|
-
return new Promise((resolve, reject) => {
|
|
403
|
-
chrome.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
407
|
-
});
|
|
863
|
+
return new Promise((resolve, reject) => chrome.debugger.detach(target, () => {
|
|
864
|
+
const error = chrome.runtime.lastError;
|
|
865
|
+
error ? reject(new Error(error.message)) : resolve();
|
|
866
|
+
}));
|
|
408
867
|
}
|
|
409
868
|
__name(debuggerDetach, "debuggerDetach");
|
|
410
|
-
function sendCommand(target, method, params) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
869
|
+
function sendCommand(target, method, params, signal) {
|
|
870
|
+
throwIfAborted(signal);
|
|
871
|
+
return withAbort(new Promise((resolve, reject) => chrome.debugger.sendCommand(target, method, params || {}, (result) => {
|
|
872
|
+
const error = chrome.runtime.lastError;
|
|
873
|
+
if (error) reject(new Error(error.message));
|
|
874
|
+
else if (result?.exceptionDetails) reject(new Error("Page script could not be evaluated"));
|
|
875
|
+
else resolve(result || {});
|
|
876
|
+
})), signal);
|
|
417
877
|
}
|
|
418
878
|
__name(sendCommand, "sendCommand");
|
|
419
|
-
function waitForLoad(tabId, timeoutMs) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
}, "finish");
|
|
430
|
-
const listener = /* @__PURE__ */ __name((updatedTabId, changeInfo) => {
|
|
431
|
-
if (updatedTabId === tabId && changeInfo.status === "complete") {
|
|
432
|
-
finish(resolve);
|
|
433
|
-
}
|
|
434
|
-
}, "listener");
|
|
435
|
-
const timer = setTimeout(() => finish(reject, new Error("Page load timeout")), timeoutMs);
|
|
436
|
-
chrome.tabs.onUpdated.addListener(listener);
|
|
437
|
-
chrome.tabs.get(tabId).then((tab) => {
|
|
438
|
-
if (tab.status === "complete") {
|
|
439
|
-
finish(resolve);
|
|
440
|
-
}
|
|
441
|
-
}).catch(() => {
|
|
442
|
-
});
|
|
443
|
-
});
|
|
879
|
+
async function waitForLoad(tabId, timeoutMs, signal) {
|
|
880
|
+
const start = Date.now();
|
|
881
|
+
while (Date.now() - start < timeoutMs) {
|
|
882
|
+
throwIfAborted(signal);
|
|
883
|
+
const tab = await chrome.tabs.get(tabId);
|
|
884
|
+
if (tab.status === "complete") return;
|
|
885
|
+
await delay(250, signal);
|
|
886
|
+
}
|
|
887
|
+
throw new Error("Page load timeout");
|
|
444
888
|
}
|
|
445
889
|
__name(waitForLoad, "waitForLoad");
|
|
890
|
+
async function evaluatePage(target, fn, args, signal) {
|
|
891
|
+
const result = await sendCommand(target, "Runtime.evaluate", {
|
|
892
|
+
expression: `(${fn.toString()})(${JSON.stringify(args ?? null)})`,
|
|
893
|
+
returnByValue: true
|
|
894
|
+
}, signal);
|
|
895
|
+
return result.result?.value;
|
|
896
|
+
}
|
|
897
|
+
__name(evaluatePage, "evaluatePage");
|
|
898
|
+
function watchNetwork(target) {
|
|
899
|
+
const pending = /* @__PURE__ */ new Set();
|
|
900
|
+
let changedAt = Date.now();
|
|
901
|
+
const listener = /* @__PURE__ */ __name((source, method, params) => {
|
|
902
|
+
if (source.tabId !== target.tabId) return;
|
|
903
|
+
if (method === "Network.requestWillBeSent" && [
|
|
904
|
+
"Document",
|
|
905
|
+
"XHR",
|
|
906
|
+
"Fetch",
|
|
907
|
+
"Stylesheet",
|
|
908
|
+
"Font"
|
|
909
|
+
].includes(params.type)) {
|
|
910
|
+
pending.add(params.requestId);
|
|
911
|
+
changedAt = Date.now();
|
|
912
|
+
} else if ([
|
|
913
|
+
"Network.loadingFinished",
|
|
914
|
+
"Network.loadingFailed"
|
|
915
|
+
].includes(method) && pending.delete(params.requestId)) changedAt = Date.now();
|
|
916
|
+
}, "listener");
|
|
917
|
+
chrome.debugger.onEvent.addListener(listener);
|
|
918
|
+
return {
|
|
919
|
+
busy: /* @__PURE__ */ __name(() => pending.size > 0 || Date.now() - changedAt < 750, "busy"),
|
|
920
|
+
reset: /* @__PURE__ */ __name(() => {
|
|
921
|
+
pending.clear();
|
|
922
|
+
changedAt = Date.now();
|
|
923
|
+
}, "reset"),
|
|
924
|
+
dispose: /* @__PURE__ */ __name(() => chrome.debugger.onEvent.removeListener(listener), "dispose")
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
__name(watchNetwork, "watchNetwork");
|
|
928
|
+
async function preparePageForCapture(target, signal) {
|
|
929
|
+
await sendCommand(target, "Emulation.setFocusEmulationEnabled", {
|
|
930
|
+
enabled: true
|
|
931
|
+
}, signal).catch((error) => {
|
|
932
|
+
throwIfAborted(signal);
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
__name(preparePageForCapture, "preparePageForCapture");
|
|
936
|
+
function pageMetrics({ maxSeenY = 0 } = {}) {
|
|
937
|
+
function visible(el) {
|
|
938
|
+
if (!el?.getBoundingClientRect) return false;
|
|
939
|
+
const rect = el.getBoundingClientRect();
|
|
940
|
+
if (rect.width <= 2 || rect.height <= 2 || rect.top > maxSeenY + innerHeight) return false;
|
|
941
|
+
for (let node = el; node instanceof Element; node = node.parentElement) {
|
|
942
|
+
const style = getComputedStyle(node);
|
|
943
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) < 0.05) return false;
|
|
944
|
+
}
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
__name(visible, "visible");
|
|
948
|
+
const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
|
|
949
|
+
const strong = '[aria-busy="true"],.ant-spin-spinning,.ant-skeleton,.mantine-Loader-root,.MuiCircularProgress-root,.v-progress-circular--indeterminate,.el-loading-mask,.chakra-spinner,[role="progressbar"]:not([aria-valuenow])';
|
|
950
|
+
let loading = [
|
|
951
|
+
...document.querySelectorAll(strong)
|
|
952
|
+
].some(visible);
|
|
953
|
+
if (!loading) loading = [
|
|
954
|
+
...document.querySelectorAll(".loading,.loader,.spinner,.skeleton")
|
|
955
|
+
].some((el) => {
|
|
956
|
+
if (!visible(el)) return false;
|
|
957
|
+
const words = (el.innerText || "").trim();
|
|
958
|
+
const rect = el.getBoundingClientRect();
|
|
959
|
+
return words.length < 160 && /^(loading\b|please wait\b|加载中|正在加载|読み込み中|로딩)/i.test(words) || !words && rect.width <= 256 && rect.height <= 256 && getComputedStyle(el).animationName !== "none";
|
|
960
|
+
});
|
|
961
|
+
const pendingImages = [
|
|
962
|
+
...document.images
|
|
963
|
+
].filter((img) => !img.complete && visible(img) && img.width * img.height > 4e3).length;
|
|
964
|
+
const fontsPending = document.fonts?.status === "loading";
|
|
965
|
+
let hash = 0;
|
|
966
|
+
for (let i = 0; i < text.length; i += Math.max(1, Math.floor(text.length / 2e3))) hash = Math.imul(hash, 31) + text.charCodeAt(i) | 0;
|
|
967
|
+
const height = Math.max(document.body?.scrollHeight || 0, document.documentElement.scrollHeight);
|
|
968
|
+
return {
|
|
969
|
+
complete: document.readyState === "complete",
|
|
970
|
+
loading,
|
|
971
|
+
pendingImages,
|
|
972
|
+
fontsPending,
|
|
973
|
+
signature: [
|
|
974
|
+
location.href,
|
|
975
|
+
text.length,
|
|
976
|
+
hash,
|
|
977
|
+
height,
|
|
978
|
+
pendingImages,
|
|
979
|
+
fontsPending
|
|
980
|
+
].join("|")
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
__name(pageMetrics, "pageMetrics");
|
|
984
|
+
async function waitForPageStable(target, options, signal, network, maxSeenY = 0) {
|
|
985
|
+
const start = Date.now();
|
|
986
|
+
let signature = "", stableSince = start, last;
|
|
987
|
+
while (Date.now() - start < options.readyTimeoutMs) {
|
|
988
|
+
throwIfAborted(signal);
|
|
989
|
+
last = await evaluatePage(target, pageMetrics, {
|
|
990
|
+
maxSeenY
|
|
991
|
+
}, signal);
|
|
992
|
+
if (!last) throw new Error("Could not inspect page readiness");
|
|
993
|
+
if (last.signature !== signature || !last.complete || last.loading || last.pendingImages || last.fontsPending || network?.busy()) stableSince = Date.now();
|
|
994
|
+
signature = last.signature;
|
|
995
|
+
if (Date.now() - stableSince >= 1e3) return "";
|
|
996
|
+
await delay(250, signal);
|
|
997
|
+
}
|
|
998
|
+
const reason = last?.loading ? "Page still shows loading UI" : "Page content is still changing or loading";
|
|
999
|
+
if (options.allowUnstable) return `${reason}; captured after the readiness timeout at your request.`;
|
|
1000
|
+
throw new Error(`${reason}. Increase readiness wait, or choose Capture after timeout in Settings if this page is ready.`);
|
|
1001
|
+
}
|
|
1002
|
+
__name(waitForPageStable, "waitForPageStable");
|
|
446
1003
|
function challengeError(name) {
|
|
447
1004
|
const error = new Error(`Blocked by ${name} verification`);
|
|
448
1005
|
error.isChallenge = true;
|
|
@@ -453,135 +1010,100 @@ function isChallengeError(error) {
|
|
|
453
1010
|
return Boolean(error?.isChallenge);
|
|
454
1011
|
}
|
|
455
1012
|
__name(isChallengeError, "isChallengeError");
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return
|
|
1013
|
+
function pageProbe() {
|
|
1014
|
+
if (document.documentURI.startsWith("chrome-error://")) return {
|
|
1015
|
+
error: "Page failed to load (DNS or network error)",
|
|
1016
|
+
challenge: ""
|
|
1017
|
+
};
|
|
1018
|
+
const shown = /* @__PURE__ */ __name((selector) => [
|
|
1019
|
+
...document.querySelectorAll(selector)
|
|
1020
|
+
].some((el) => {
|
|
1021
|
+
const r = el.getBoundingClientRect();
|
|
1022
|
+
return r.width > 0 && r.height > 0 && el.checkVisibility({
|
|
1023
|
+
checkOpacity: true,
|
|
1024
|
+
checkVisibilityCSS: true
|
|
1025
|
+
});
|
|
1026
|
+
}), "shown");
|
|
1027
|
+
const title = document.title.trim().toLowerCase();
|
|
1028
|
+
let challenge = "";
|
|
1029
|
+
if (shown("#challenge-form,#challenge-stage,#cf-challenge-running") && /just a moment|checking your browser|attention required|verify/.test(title)) challenge = "Cloudflare";
|
|
1030
|
+
else if (shown("#vercel-security-checkpoint,#challenge-container") && /security checkpoint|verifying your browser|安全检查点/.test(title)) challenge = "Vercel";
|
|
1031
|
+
else if (/^(verify you are human|human verification|are you a robot|captcha|人机验证)[\s.!?—-]*$/i.test(title) && shown('iframe[src*="recaptcha"],iframe[src*="hcaptcha"],iframe[src*="challenges.cloudflare.com"],input[name*="captcha"],[data-sitekey]')) challenge = "bot-check";
|
|
1032
|
+
return {
|
|
1033
|
+
error: "",
|
|
1034
|
+
challenge
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
__name(pageProbe, "pageProbe");
|
|
1038
|
+
async function probePage(target, signal) {
|
|
1039
|
+
return await evaluatePage(target, pageProbe, null, signal) || {
|
|
476
1040
|
error: "",
|
|
477
1041
|
challenge: ""
|
|
478
1042
|
};
|
|
479
1043
|
}
|
|
480
1044
|
__name(probePage, "probePage");
|
|
481
|
-
async function waitForVerification(tabId, shouldStop, timeoutMs = 18e4) {
|
|
1045
|
+
async function waitForVerification(tabId, shouldStop, signal, timeoutMs = 18e4) {
|
|
482
1046
|
const start = Date.now();
|
|
483
1047
|
while (Date.now() - start < timeoutMs) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
await delay(2500);
|
|
1048
|
+
throwIfAborted(signal);
|
|
1049
|
+
if (shouldStop()) return false;
|
|
1050
|
+
await delay(2500, signal);
|
|
488
1051
|
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
489
|
-
if (!tab)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1052
|
+
if (!tab) return false;
|
|
1053
|
+
if (tab.status !== "complete") continue;
|
|
1054
|
+
const target = {
|
|
1055
|
+
tabId
|
|
1056
|
+
};
|
|
1057
|
+
let attached = false;
|
|
1058
|
+
try {
|
|
1059
|
+
await debuggerAttach(target, "1.3");
|
|
1060
|
+
attached = true;
|
|
1061
|
+
const result = await probePage(target, signal);
|
|
1062
|
+
if (!result.challenge && !result.error) return true;
|
|
1063
|
+
} catch {
|
|
1064
|
+
throwIfAborted(signal);
|
|
1065
|
+
} finally {
|
|
1066
|
+
if (attached) await debuggerDetach(target).catch(() => {
|
|
1067
|
+
});
|
|
499
1068
|
}
|
|
500
1069
|
}
|
|
501
1070
|
return false;
|
|
502
1071
|
}
|
|
503
1072
|
__name(waitForVerification, "waitForVerification");
|
|
504
1073
|
async function focusTab(tabId) {
|
|
505
|
-
await chrome.tabs.update(tabId, {
|
|
1074
|
+
const tab = await chrome.tabs.update(tabId, {
|
|
506
1075
|
active: true
|
|
507
|
-
}).catch(() => {
|
|
508
1076
|
});
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
await chrome.windows.update(tab.windowId, {
|
|
513
|
-
focused: true
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
} catch {
|
|
517
|
-
}
|
|
1077
|
+
await chrome.windows.update(tab.windowId, {
|
|
1078
|
+
focused: true
|
|
1079
|
+
});
|
|
518
1080
|
}
|
|
519
1081
|
__name(focusTab, "focusTab");
|
|
520
|
-
function autoScroll(target) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
1082
|
+
async function autoScroll(target, signal) {
|
|
1083
|
+
let maxSeenY = 0;
|
|
1084
|
+
for (let step = 0; step < 60; step++) {
|
|
1085
|
+
const position = await evaluatePage(target, () => {
|
|
1086
|
+
window.scrollBy({
|
|
1087
|
+
top: 600,
|
|
1088
|
+
behavior: "instant"
|
|
1089
|
+
});
|
|
1090
|
+
return {
|
|
1091
|
+
y: scrollY,
|
|
1092
|
+
bottom: scrollY + innerHeight >= Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0) - 1
|
|
1093
|
+
};
|
|
1094
|
+
}, null, signal);
|
|
1095
|
+
maxSeenY = Math.max(maxSeenY, position.y);
|
|
1096
|
+
await delay(120, signal);
|
|
1097
|
+
if (position.bottom) break;
|
|
1098
|
+
}
|
|
1099
|
+
await evaluatePage(target, () => window.scrollTo({
|
|
1100
|
+
left: 0,
|
|
1101
|
+
top: 0,
|
|
1102
|
+
behavior: "instant"
|
|
1103
|
+
}), null, signal);
|
|
1104
|
+
return maxSeenY;
|
|
537
1105
|
}
|
|
538
1106
|
__name(autoScroll, "autoScroll");
|
|
539
|
-
function revealAnimations(target, reversible = false) {
|
|
540
|
-
return sendCommand(target, "Runtime.evaluate", {
|
|
541
|
-
expression: `(function(){
|
|
542
|
-
try{
|
|
543
|
-
var reversible=${reversible ? "true" : "false"};
|
|
544
|
-
var state=reversible?{style:null,aos:[],inline:[]}:null;
|
|
545
|
-
var css=document.createElement('style');
|
|
546
|
-
css.textContent=[
|
|
547
|
-
'[data-aos]{opacity:1!important;transform:none!important;transition:none!important;}',
|
|
548
|
-
'.wow{visibility:visible!important;opacity:1!important;animation-name:none!important;}',
|
|
549
|
-
'.animate__animated,.animated{opacity:1!important;}',
|
|
550
|
-
'[data-scroll]{opacity:1!important;transform:none!important;}',
|
|
551
|
-
'.reveal,[data-sr],[data-sr-id],.sr-reveal{opacity:1!important;transform:none!important;visibility:visible!important;}'
|
|
552
|
-
].join('');
|
|
553
|
-
(document.head||document.documentElement).appendChild(css);
|
|
554
|
-
if(state) state.style=css;
|
|
555
|
-
document.querySelectorAll('[data-aos]').forEach(function(el){
|
|
556
|
-
if(state&&!el.classList.contains('aos-animate')) state.aos.push(el);
|
|
557
|
-
el.classList.add('aos-animate');
|
|
558
|
-
});
|
|
559
|
-
// \u901A\u7528\u515C\u5E95\uFF1A\u88AB\u52A8\u753B\u85CF\u8D77\u6765\u7684\u5185\u5BB9\u5757\uFF08opacity\u22480 \u4E14\u5E26 transition/animation\uFF09\u5F3A\u5236\u663E\u793A\u3002
|
|
560
|
-
// \u53EA\u5904\u7406 static/relative\uFF08\u666E\u901A\u5185\u5BB9\u6D41\uFF09\uFF0C\u8DF3\u8FC7 fixed/absolute\uFF08\u591A\u4E3A\u5F39\u7A97/\u83DC\u5355/\u8F6E\u64AD\uFF09\uFF0C\u907F\u514D\u8BEF\u4F24
|
|
561
|
-
var all=document.body?document.body.querySelectorAll('*'):[];
|
|
562
|
-
for(var i=0;i<all.length;i++){
|
|
563
|
-
var el=all[i], s=getComputedStyle(el);
|
|
564
|
-
if(parseFloat(s.opacity)<0.05 && (s.transitionDuration!=='0s'||s.animationName!=='none')){
|
|
565
|
-
var p=s.position;
|
|
566
|
-
if(p==='static'||p==='relative'){
|
|
567
|
-
if(state) state.inline.push({
|
|
568
|
-
el:el,
|
|
569
|
-
opacity:el.style.getPropertyValue('opacity'),
|
|
570
|
-
opacityPriority:el.style.getPropertyPriority('opacity'),
|
|
571
|
-
transform:el.style.getPropertyValue('transform'),
|
|
572
|
-
transformPriority:el.style.getPropertyPriority('transform')
|
|
573
|
-
});
|
|
574
|
-
el.style.setProperty('opacity','1','important');
|
|
575
|
-
el.style.setProperty('transform','none','important');
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
if(state) window.__zuckerExtRevealState=state;
|
|
580
|
-
}catch(e){}
|
|
581
|
-
})()`
|
|
582
|
-
}).catch(() => void 0);
|
|
583
|
-
}
|
|
584
|
-
__name(revealAnimations, "revealAnimations");
|
|
585
1107
|
async function getScrollPosition(target) {
|
|
586
1108
|
const result = await sendCommand(target, "Runtime.evaluate", {
|
|
587
1109
|
returnByValue: true,
|
|
@@ -594,76 +1116,89 @@ async function getScrollPosition(target) {
|
|
|
594
1116
|
};
|
|
595
1117
|
}
|
|
596
1118
|
__name(getScrollPosition, "getScrollPosition");
|
|
1119
|
+
function hidePageCaptureDecorations() {
|
|
1120
|
+
const hosts = [
|
|
1121
|
+
...document.querySelectorAll("browser-mcp-container")
|
|
1122
|
+
];
|
|
1123
|
+
window.__zuckerExtCaptureDecorations = hosts.map((el) => ({
|
|
1124
|
+
el,
|
|
1125
|
+
display: el.style.getPropertyValue("display"),
|
|
1126
|
+
priority: el.style.getPropertyPriority("display")
|
|
1127
|
+
}));
|
|
1128
|
+
hosts.forEach((el) => el.style.setProperty("display", "none", "important"));
|
|
1129
|
+
return {
|
|
1130
|
+
width: innerWidth,
|
|
1131
|
+
height: innerHeight,
|
|
1132
|
+
dpr: devicePixelRatio || 1
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
__name(hidePageCaptureDecorations, "hidePageCaptureDecorations");
|
|
1136
|
+
function showPageCaptureDecorations() {
|
|
1137
|
+
for (const saved of window.__zuckerExtCaptureDecorations || []) {
|
|
1138
|
+
if (saved.display) saved.el.style.setProperty("display", saved.display, saved.priority);
|
|
1139
|
+
else saved.el.style.removeProperty("display");
|
|
1140
|
+
}
|
|
1141
|
+
delete window.__zuckerExtCaptureDecorations;
|
|
1142
|
+
}
|
|
1143
|
+
__name(showPageCaptureDecorations, "showPageCaptureDecorations");
|
|
1144
|
+
function suppressCaptureDecorations(target, signal) {
|
|
1145
|
+
return sendCommand(target, "Runtime.evaluate", {
|
|
1146
|
+
expression: `(${hidePageCaptureDecorations.toString()})()`
|
|
1147
|
+
}, signal);
|
|
1148
|
+
}
|
|
1149
|
+
__name(suppressCaptureDecorations, "suppressCaptureDecorations");
|
|
1150
|
+
function restoreCaptureDecorations(target) {
|
|
1151
|
+
return sendCommand(target, "Runtime.evaluate", {
|
|
1152
|
+
expression: `(${showPageCaptureDecorations.toString()})()`
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
__name(restoreCaptureDecorations, "restoreCaptureDecorations");
|
|
597
1156
|
function restorePageAfterQuick(target, position) {
|
|
598
1157
|
const x = Number(position?.x) || 0;
|
|
599
1158
|
const y = Number(position?.y) || 0;
|
|
600
1159
|
return sendCommand(target, "Runtime.evaluate", {
|
|
601
|
-
expression: `(
|
|
602
|
-
try{
|
|
603
|
-
var state=window.__zuckerExtRevealState;
|
|
604
|
-
if(state){
|
|
605
|
-
if(state.style&&state.style.remove) state.style.remove();
|
|
606
|
-
(state.aos||[]).forEach(function(el){ el.classList.remove('aos-animate'); });
|
|
607
|
-
(state.inline||[]).forEach(function(saved){
|
|
608
|
-
if(saved.opacity) saved.el.style.setProperty('opacity',saved.opacity,saved.opacityPriority||'');
|
|
609
|
-
else saved.el.style.removeProperty('opacity');
|
|
610
|
-
if(saved.transform) saved.el.style.setProperty('transform',saved.transform,saved.transformPriority||'');
|
|
611
|
-
else saved.el.style.removeProperty('transform');
|
|
612
|
-
});
|
|
613
|
-
delete window.__zuckerExtRevealState;
|
|
614
|
-
}
|
|
615
|
-
}catch(e){}
|
|
616
|
-
window.scrollTo(${x}, ${y});
|
|
617
|
-
})()`
|
|
1160
|
+
expression: `window.scrollTo({ left: ${x}, top: ${y}, behavior: "instant" })`
|
|
618
1161
|
});
|
|
619
1162
|
}
|
|
620
1163
|
__name(restorePageAfterQuick, "restorePageAfterQuick");
|
|
621
1164
|
|
|
622
|
-
// src/engine.
|
|
623
|
-
var LOAD_TIMEOUT_MS = 3e4;
|
|
624
|
-
var SAME_HOST_MIN_GAP_MS = 1800;
|
|
625
|
-
var DEFAULT_TEXTS = {
|
|
626
|
-
alreadyRunning: "Another capture is already running",
|
|
627
|
-
cannotQuick: "This page cannot be captured. Open an http(s) website first.",
|
|
628
|
-
challengeBeforeQuick: /* @__PURE__ */ __name((challenge) => `Complete the ${challenge} verification before capturing this page.`, "challengeBeforeQuick"),
|
|
629
|
-
quickFolder: "Quick Captures"
|
|
630
|
-
};
|
|
1165
|
+
// src/engine.js
|
|
631
1166
|
function initExtension(config) {
|
|
1167
|
+
if (!Number.isSafeInteger(config.freeDailyLimit) || config.freeDailyLimit < 0) throw new RangeError("freeDailyLimit must be an explicit non-negative integer");
|
|
1168
|
+
const normalizeSettings = /* @__PURE__ */ __name((defaults, input) => {
|
|
1169
|
+
const settings = config.normalizeSettings ? config.normalizeSettings(defaults, input) : {
|
|
1170
|
+
...defaults,
|
|
1171
|
+
...input && typeof input === "object" && !Array.isArray(input) ? input : {}
|
|
1172
|
+
};
|
|
1173
|
+
return {
|
|
1174
|
+
...settings,
|
|
1175
|
+
delayMs: Math.min(1e4, Math.max(0, Number(settings.delayMs) || 0)),
|
|
1176
|
+
readyTimeoutMs: Math.min(6e4, Math.max(5e3, Number(settings.readyTimeoutMs) || 25e3)),
|
|
1177
|
+
allowUnstable: settings.allowUnstable === true,
|
|
1178
|
+
ignoreChallenge: settings.ignoreChallenge === true,
|
|
1179
|
+
expandScrollAreas: settings.expandScrollAreas !== false
|
|
1180
|
+
};
|
|
1181
|
+
}, "normalizeSettings");
|
|
1182
|
+
const scrollCapture = config.scrollCapture || scroll_capture_exports;
|
|
632
1183
|
const texts = {
|
|
633
|
-
|
|
634
|
-
|
|
1184
|
+
alreadyRunning: "Another capture is already running",
|
|
1185
|
+
cannotQuick: "Open an http(s) website first.",
|
|
1186
|
+
challengeBeforeQuick: /* @__PURE__ */ __name((name) => `Complete the ${name} verification, or enable Capture verification page in Settings if this is a normal page.`, "challengeBeforeQuick"),
|
|
1187
|
+
quickFolder: "Quick Captures",
|
|
1188
|
+
...config.texts
|
|
635
1189
|
};
|
|
636
|
-
const freeDailyLimit = config.freeDailyLimit ?? 30;
|
|
637
|
-
const USAGE_KEY = `${config.product}_free_usage`;
|
|
638
|
-
const BATCH_KEY = `${config.product}_batch_state`;
|
|
639
1190
|
const prefix = config.messagePrefix;
|
|
1191
|
+
const BATCH_KEY = `${config.product}_batch_state`, QUICK_KEY = `${config.product}_quick_state`;
|
|
1192
|
+
const USAGE_KEY = `${config.product}_free_usage`, TAB_KEY = `${config.product}_owned_tab`;
|
|
640
1193
|
const licensing = createLicensing(config.product, config.brand, config.licensing);
|
|
641
|
-
const downloader = createDownloader(`
|
|
1194
|
+
const downloader = createDownloader(`Prepare local ${config.brand} downloads.`, config.downloads);
|
|
1195
|
+
let activeRun = null;
|
|
642
1196
|
let batch = idleBatch();
|
|
643
|
-
let restorePromise
|
|
644
|
-
let quickRunning = false;
|
|
645
|
-
chrome.runtime.onInstalled.addListener(() => {
|
|
646
|
-
const previousRestore = restorePromise;
|
|
647
|
-
restorePromise = (async () => {
|
|
648
|
-
await previousRestore.catch(() => {
|
|
649
|
-
});
|
|
650
|
-
batch = idleBatch();
|
|
651
|
-
await storageRemove(BATCH_KEY);
|
|
652
|
-
setBadge("");
|
|
653
|
-
})();
|
|
654
|
-
});
|
|
655
|
-
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
656
|
-
if (message?.target === "offscreen") {
|
|
657
|
-
return false;
|
|
658
|
-
}
|
|
659
|
-
handleMessage(message).then(sendResponse).catch((error) => sendResponse({
|
|
660
|
-
ok: false,
|
|
661
|
-
error: normalizeError(error)
|
|
662
|
-
}));
|
|
663
|
-
return true;
|
|
664
|
-
});
|
|
1197
|
+
let restorePromise;
|
|
665
1198
|
function idleBatch() {
|
|
666
1199
|
return {
|
|
1200
|
+
version: 2,
|
|
1201
|
+
id: crypto.randomUUID(),
|
|
667
1202
|
running: false,
|
|
668
1203
|
cancelRequested: false,
|
|
669
1204
|
startedAt: null,
|
|
@@ -672,529 +1207,762 @@ function initExtension(config) {
|
|
|
672
1207
|
done: 0,
|
|
673
1208
|
success: 0,
|
|
674
1209
|
failed: 0,
|
|
1210
|
+
cancelled: 0,
|
|
675
1211
|
current: "",
|
|
676
1212
|
freeLimited: false,
|
|
677
1213
|
needVerify: null,
|
|
678
|
-
|
|
679
|
-
settings: {
|
|
680
|
-
...config.defaultSettings
|
|
681
|
-
},
|
|
1214
|
+
settings: normalizeSettings(config.defaultSettings),
|
|
682
1215
|
pro: false,
|
|
683
1216
|
items: []
|
|
684
1217
|
};
|
|
685
1218
|
}
|
|
686
1219
|
__name(idleBatch, "idleBatch");
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
return cancelBatch();
|
|
698
|
-
case `${prefix}_CLEAR_BATCH`:
|
|
699
|
-
return clearBatch();
|
|
700
|
-
case `${prefix}_SKIP_VERIFY`:
|
|
701
|
-
batch.skipVerify = true;
|
|
702
|
-
await persistBatch();
|
|
703
|
-
return {
|
|
704
|
-
ok: true
|
|
705
|
-
};
|
|
706
|
-
case `${prefix}_STATE`:
|
|
707
|
-
return {
|
|
708
|
-
ok: true,
|
|
709
|
-
batch: publicBatch(),
|
|
710
|
-
quota: await getQuota()
|
|
711
|
-
};
|
|
712
|
-
case `${prefix}_ACTIVATE`:
|
|
713
|
-
await licensing.activate(message.code);
|
|
714
|
-
return {
|
|
715
|
-
ok: true,
|
|
716
|
-
quota: await getQuota()
|
|
717
|
-
};
|
|
718
|
-
case `${prefix}_CLEAR_LICENSE`:
|
|
719
|
-
await licensing.clear();
|
|
720
|
-
return {
|
|
721
|
-
ok: true,
|
|
722
|
-
quota: await getQuota()
|
|
723
|
-
};
|
|
724
|
-
default:
|
|
725
|
-
return {
|
|
726
|
-
ok: false,
|
|
727
|
-
error: "Unknown message"
|
|
728
|
-
};
|
|
729
|
-
}
|
|
1220
|
+
function reserveRun(restoring = false) {
|
|
1221
|
+
if (activeRun || !restoring && batch.running) return null;
|
|
1222
|
+
activeRun = {
|
|
1223
|
+
controller: new AbortController(),
|
|
1224
|
+
job: null,
|
|
1225
|
+
skipVerify: false,
|
|
1226
|
+
windowId: null,
|
|
1227
|
+
idleTabId: null
|
|
1228
|
+
};
|
|
1229
|
+
return activeRun;
|
|
730
1230
|
}
|
|
731
|
-
__name(
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
pro: true,
|
|
737
|
-
expiresAt: entitlement.expiresAt,
|
|
738
|
-
limit: null,
|
|
739
|
-
used: 0,
|
|
740
|
-
remaining: 999999
|
|
741
|
-
};
|
|
1231
|
+
__name(reserveRun, "reserveRun");
|
|
1232
|
+
function releaseRun(run) {
|
|
1233
|
+
if (activeRun === run) {
|
|
1234
|
+
activeRun = null;
|
|
1235
|
+
setBadge("");
|
|
742
1236
|
}
|
|
743
|
-
|
|
1237
|
+
}
|
|
1238
|
+
__name(releaseRun, "releaseRun");
|
|
1239
|
+
function recount(job) {
|
|
1240
|
+
job.success = job.items.filter((i) => i.status === "success").length;
|
|
1241
|
+
job.failed = job.items.filter((i) => i.status === "failed").length;
|
|
1242
|
+
job.cancelled = job.items.filter((i) => i.status === "cancelled").length;
|
|
1243
|
+
job.done = job.success + job.failed + job.cancelled;
|
|
1244
|
+
}
|
|
1245
|
+
__name(recount, "recount");
|
|
1246
|
+
function publicBatch() {
|
|
1247
|
+
return {
|
|
1248
|
+
...batch,
|
|
1249
|
+
busy: Boolean(activeRun),
|
|
1250
|
+
// Marked running with no live controller: a final write failed and left
|
|
1251
|
+
// downloads unconfirmed. The popup must stay usable in this state.
|
|
1252
|
+
stalled: Boolean(batch.running && !activeRun),
|
|
1253
|
+
items: batch.items.map(({ url, index, status, fileName, error, warning }) => ({
|
|
1254
|
+
url,
|
|
1255
|
+
index,
|
|
1256
|
+
status,
|
|
1257
|
+
fileName,
|
|
1258
|
+
error,
|
|
1259
|
+
warning
|
|
1260
|
+
}))
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
__name(publicBatch, "publicBatch");
|
|
1264
|
+
const persist = /* @__PURE__ */ __name((job) => storageSet({
|
|
1265
|
+
[BATCH_KEY]: job
|
|
1266
|
+
}), "persist");
|
|
1267
|
+
function usageFrom(data) {
|
|
744
1268
|
const usage = data[USAGE_KEY];
|
|
745
|
-
|
|
746
|
-
|
|
1269
|
+
return usage?.date === todayString() && Number.isSafeInteger(usage.used) && usage.used >= 0 ? usage.used : 0;
|
|
1270
|
+
}
|
|
1271
|
+
__name(usageFrom, "usageFrom");
|
|
1272
|
+
async function getQuota(cachedOnly = false, force = false) {
|
|
1273
|
+
const status = await licensing.getStatus({
|
|
1274
|
+
cachedOnly,
|
|
1275
|
+
force
|
|
1276
|
+
});
|
|
1277
|
+
const license = {
|
|
1278
|
+
licenseStatus: status.state,
|
|
1279
|
+
licenseMessage: status.message || "",
|
|
1280
|
+
licenseRefreshing: Boolean(status.refreshing),
|
|
1281
|
+
hasLicense: status.state !== "free"
|
|
1282
|
+
};
|
|
1283
|
+
if (status.entitlement) return {
|
|
1284
|
+
...license,
|
|
1285
|
+
pro: true,
|
|
1286
|
+
expiresAt: status.entitlement.expiresAt,
|
|
1287
|
+
limit: null,
|
|
1288
|
+
used: 0,
|
|
1289
|
+
remaining: null
|
|
1290
|
+
};
|
|
1291
|
+
if (status.state !== "free") return {
|
|
1292
|
+
...license,
|
|
1293
|
+
pro: false,
|
|
1294
|
+
limit: null,
|
|
1295
|
+
used: 0,
|
|
1296
|
+
remaining: null
|
|
1297
|
+
};
|
|
1298
|
+
const used = usageFrom(await storageGet(USAGE_KEY));
|
|
1299
|
+
const limit = config.freeDailyLimit;
|
|
747
1300
|
return {
|
|
1301
|
+
...license,
|
|
748
1302
|
pro: false,
|
|
749
|
-
limit
|
|
1303
|
+
limit,
|
|
750
1304
|
used,
|
|
751
|
-
remaining: Math.max(0,
|
|
1305
|
+
remaining: Math.max(0, limit - used)
|
|
752
1306
|
};
|
|
753
1307
|
}
|
|
754
1308
|
__name(getQuota, "getQuota");
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const used = usage && usage.date === today ? usage.used || 0 : 0;
|
|
760
|
-
await storageSet({
|
|
761
|
-
[USAGE_KEY]: {
|
|
762
|
-
date: today,
|
|
763
|
-
used: used + count
|
|
764
|
-
}
|
|
765
|
-
});
|
|
1309
|
+
function assertQuota(quota) {
|
|
1310
|
+
if (quota.pro) return;
|
|
1311
|
+
if (quota.hasLicense) throw new Error(quota.licenseMessage || "Checking your Pro license. Use Recheck Pro in License and retry.");
|
|
1312
|
+
if (!quota.remaining) throw new Error(`Daily free limit reached (${quota.limit} pages/day). Already purchased Pro? Enter your original activation code in License to restore unlimited exports.`);
|
|
766
1313
|
}
|
|
767
|
-
__name(
|
|
768
|
-
async function
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
const normalized = normalizeUrls(urls);
|
|
776
|
-
if (!normalized.length) {
|
|
777
|
-
return {
|
|
778
|
-
ok: false,
|
|
779
|
-
error: "No valid http(s) URLs"
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
const quota = await getQuota();
|
|
783
|
-
if (!quota.pro && quota.remaining <= 0) {
|
|
784
|
-
return {
|
|
785
|
-
ok: false,
|
|
786
|
-
error: `Daily free limit reached (${quota.limit} pages/day). Go Pro for unlimited pages.`
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
const freeLimited = !quota.pro && normalized.length > quota.remaining;
|
|
790
|
-
const accepted = quota.pro ? normalized : normalized.slice(0, quota.remaining);
|
|
791
|
-
const options = {
|
|
792
|
-
...config.defaultSettings,
|
|
793
|
-
...settings || {}
|
|
1314
|
+
__name(assertQuota, "assertQuota");
|
|
1315
|
+
async function commitResult(job, item, status, error = "", quick = false) {
|
|
1316
|
+
const previous = {
|
|
1317
|
+
status: item.status,
|
|
1318
|
+
error: item.error,
|
|
1319
|
+
usageRecorded: item.usageRecorded
|
|
794
1320
|
};
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
})
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1321
|
+
let used;
|
|
1322
|
+
if (status === "success" && item.chargeFree && !item.usageRecorded) {
|
|
1323
|
+
used = usageFrom(await storageGet(USAGE_KEY)) + 1;
|
|
1324
|
+
}
|
|
1325
|
+
item.status = status;
|
|
1326
|
+
item.error = error;
|
|
1327
|
+
if (used !== void 0) item.usageRecorded = true;
|
|
1328
|
+
if (!quick) recount(job);
|
|
1329
|
+
try {
|
|
1330
|
+
await storageSet({
|
|
1331
|
+
[quick ? QUICK_KEY : BATCH_KEY]: quick ? null : job,
|
|
1332
|
+
...used !== void 0 ? {
|
|
1333
|
+
[USAGE_KEY]: {
|
|
1334
|
+
date: todayString(),
|
|
1335
|
+
used
|
|
1336
|
+
}
|
|
1337
|
+
} : {}
|
|
1338
|
+
});
|
|
1339
|
+
} catch (error2) {
|
|
1340
|
+
Object.assign(item, previous);
|
|
1341
|
+
if (!quick) recount(job);
|
|
1342
|
+
throw error2;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
__name(commitResult, "commitResult");
|
|
1346
|
+
function makeItem(url, index) {
|
|
820
1347
|
return {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1348
|
+
url,
|
|
1349
|
+
index,
|
|
1350
|
+
status: "pending",
|
|
1351
|
+
fileName: "",
|
|
1352
|
+
error: "",
|
|
1353
|
+
warning: "",
|
|
1354
|
+
usageRecorded: false
|
|
824
1355
|
};
|
|
825
1356
|
}
|
|
826
|
-
__name(
|
|
827
|
-
async function
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1357
|
+
__name(makeItem, "makeItem");
|
|
1358
|
+
async function recoverQuick(signal) {
|
|
1359
|
+
const quick = (await storageGet(QUICK_KEY))[QUICK_KEY];
|
|
1360
|
+
if (!quick) return;
|
|
1361
|
+
if (quick.downloadId == null && !quick.downloadUrl) {
|
|
1362
|
+
await storageRemove(QUICK_KEY);
|
|
1363
|
+
return;
|
|
833
1364
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1365
|
+
let recovered = false;
|
|
1366
|
+
try {
|
|
1367
|
+
await downloader.recover(quick, signal);
|
|
1368
|
+
recovered = true;
|
|
1369
|
+
} catch {
|
|
1370
|
+
await storageRemove(QUICK_KEY);
|
|
839
1371
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1372
|
+
if (recovered) await commitResult(null, quick, "success", "", true);
|
|
1373
|
+
}
|
|
1374
|
+
__name(recoverQuick, "recoverQuick");
|
|
1375
|
+
async function startBatch(urls, settings) {
|
|
1376
|
+
const run = reserveRun();
|
|
1377
|
+
if (!run) return {
|
|
1378
|
+
ok: false,
|
|
1379
|
+
error: texts.alreadyRunning
|
|
1380
|
+
};
|
|
1381
|
+
try {
|
|
1382
|
+
await recoverQuick(run.controller.signal);
|
|
1383
|
+
const normalized = normalizeUrls(urls);
|
|
1384
|
+
if (!normalized.length) throw new Error("No valid http(s) URLs");
|
|
1385
|
+
const quota = await withAbort(getQuota(), run.controller.signal);
|
|
1386
|
+
throwIfAborted(run.controller.signal);
|
|
1387
|
+
assertQuota(quota);
|
|
1388
|
+
const accepted = quota.pro ? normalized : normalized.slice(0, quota.remaining);
|
|
1389
|
+
batch = {
|
|
1390
|
+
...idleBatch(),
|
|
1391
|
+
running: true,
|
|
1392
|
+
startedAt: Date.now(),
|
|
1393
|
+
folder: `${config.brand}/${folderNameFor(accepted)}-${crypto.randomUUID().slice(0, 6)}`,
|
|
1394
|
+
total: accepted.length,
|
|
1395
|
+
freeLimited: accepted.length < normalized.length,
|
|
1396
|
+
settings: normalizeSettings(config.defaultSettings, settings),
|
|
1397
|
+
pro: quota.pro,
|
|
1398
|
+
items: accepted.map((url, index) => makeItem(url, index + 1))
|
|
845
1399
|
};
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1400
|
+
run.job = batch;
|
|
1401
|
+
await persist(batch);
|
|
1402
|
+
void runBatch(batch, run);
|
|
849
1403
|
return {
|
|
850
|
-
ok:
|
|
851
|
-
|
|
1404
|
+
ok: true,
|
|
1405
|
+
batch: publicBatch(),
|
|
1406
|
+
quota
|
|
852
1407
|
};
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
releaseRun(run);
|
|
1410
|
+
throw error;
|
|
853
1411
|
}
|
|
854
|
-
|
|
855
|
-
|
|
1412
|
+
}
|
|
1413
|
+
__name(startBatch, "startBatch");
|
|
1414
|
+
async function runBatch(job, run, { keepLock = false } = {}) {
|
|
1415
|
+
const signal = run.controller.signal;
|
|
1416
|
+
const lastHostAt = /* @__PURE__ */ new Map();
|
|
856
1417
|
try {
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1418
|
+
for (const item of job.items) {
|
|
1419
|
+
if ([
|
|
1420
|
+
"success",
|
|
1421
|
+
"failed",
|
|
1422
|
+
"cancelled"
|
|
1423
|
+
].includes(item.status)) continue;
|
|
1424
|
+
if (item.downloadId != null || item.downloadUrl) {
|
|
1425
|
+
let error2;
|
|
1426
|
+
try {
|
|
1427
|
+
await downloader.recover(item, signal);
|
|
1428
|
+
} catch (failure) {
|
|
1429
|
+
error2 = failure;
|
|
1430
|
+
}
|
|
1431
|
+
await commitResult(job, item, error2 ? signal.aborted || error2.message === "Cancelled" ? "cancelled" : "failed" : "success", error2 ? normalizeError(error2) : "");
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
throwIfAborted(signal);
|
|
1435
|
+
const quota = await withAbort(getQuota(), signal);
|
|
1436
|
+
assertQuota(quota);
|
|
1437
|
+
const host = hostOf(item.url), previous = lastHostAt.get(host) || 0;
|
|
1438
|
+
await delay(Math.max(0, (config.sameHostGapMs ?? 1800) - (Date.now() - previous)), signal);
|
|
1439
|
+
lastHostAt.set(host, Date.now());
|
|
1440
|
+
item.chargeFree = !quota.pro;
|
|
1441
|
+
item.status = "running";
|
|
1442
|
+
job.current = item.url;
|
|
1443
|
+
setBadge(`${job.done + 1}/${job.total}`);
|
|
1444
|
+
await persist(job);
|
|
1445
|
+
let error;
|
|
1446
|
+
try {
|
|
1447
|
+
await captureUrl(item, job, run);
|
|
1448
|
+
} catch (failure) {
|
|
1449
|
+
error = failure;
|
|
1450
|
+
}
|
|
1451
|
+
await commitResult(job, item, error ? signal.aborted || error.message === "Cancelled" ? "cancelled" : "failed" : "success", error ? normalizeError(error) : "");
|
|
864
1452
|
}
|
|
865
|
-
setBadge("OK");
|
|
866
|
-
setTimeout(() => setBadge(""), 4e3);
|
|
867
|
-
return {
|
|
868
|
-
ok: true,
|
|
869
|
-
result,
|
|
870
|
-
quota: await getQuota()
|
|
871
|
-
};
|
|
872
1453
|
} catch (error) {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1454
|
+
const message = normalizeError(error);
|
|
1455
|
+
job.current = message;
|
|
1456
|
+
if (!signal.aborted && !job.cancelRequested) {
|
|
1457
|
+
for (const item of job.items) if (item.status === "pending") {
|
|
1458
|
+
item.status = "failed";
|
|
1459
|
+
item.error = message;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
880
1462
|
} finally {
|
|
881
|
-
|
|
1463
|
+
if (signal.aborted || job.cancelRequested) {
|
|
1464
|
+
job.cancelRequested = true;
|
|
1465
|
+
for (const item of job.items) if ([
|
|
1466
|
+
"pending",
|
|
1467
|
+
"running"
|
|
1468
|
+
].includes(item.status)) item.status = "cancelled";
|
|
1469
|
+
job.current = "Stopped";
|
|
1470
|
+
} else if (job.items.every((i) => [
|
|
1471
|
+
"success",
|
|
1472
|
+
"failed",
|
|
1473
|
+
"cancelled"
|
|
1474
|
+
].includes(i.status))) job.current = "Done";
|
|
1475
|
+
job.running = job.items.some((item) => item.status === "downloading");
|
|
1476
|
+
if (job.running) job.current = "Some downloads could not be confirmed. Press Stop to reconcile them.";
|
|
1477
|
+
job.needVerify = null;
|
|
1478
|
+
recount(job);
|
|
1479
|
+
try {
|
|
1480
|
+
await persist(job);
|
|
1481
|
+
} catch {
|
|
1482
|
+
console.warn(`${config.brand}: could not save final task state`);
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
await closeBatchWindow(run);
|
|
1486
|
+
} finally {
|
|
1487
|
+
if (!keepLock) releaseRun(run);
|
|
1488
|
+
}
|
|
882
1489
|
}
|
|
883
1490
|
}
|
|
884
|
-
__name(
|
|
885
|
-
async function
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
let scrollPosition = {
|
|
891
|
-
x: 0,
|
|
892
|
-
y: 0
|
|
1491
|
+
__name(runBatch, "runBatch");
|
|
1492
|
+
async function quickCapture(tabId, settings) {
|
|
1493
|
+
const run = reserveRun();
|
|
1494
|
+
if (!run) return {
|
|
1495
|
+
ok: false,
|
|
1496
|
+
error: texts.alreadyRunning
|
|
893
1497
|
};
|
|
1498
|
+
const signal = run.controller.signal;
|
|
894
1499
|
try {
|
|
895
|
-
await
|
|
896
|
-
|
|
897
|
-
await
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1500
|
+
await recoverQuick(signal);
|
|
1501
|
+
if (!Number.isInteger(tabId)) throw new Error("No active web page");
|
|
1502
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1503
|
+
if (!/^https?:\/\//i.test(tab?.url || "")) throw new Error(texts.cannotQuick);
|
|
1504
|
+
const quota = await withAbort(getQuota(), signal);
|
|
1505
|
+
assertQuota(quota);
|
|
1506
|
+
const item = {
|
|
1507
|
+
...makeItem(tab.url, 1),
|
|
1508
|
+
chargeFree: !quota.pro
|
|
1509
|
+
};
|
|
1510
|
+
const options = normalizeSettings(config.defaultSettings, settings);
|
|
1511
|
+
await storageSet({
|
|
1512
|
+
[QUICK_KEY]: item
|
|
1513
|
+
});
|
|
1514
|
+
const visibleOutput = await config.captureQuick?.({
|
|
1515
|
+
tab,
|
|
1516
|
+
options,
|
|
1517
|
+
signal
|
|
1518
|
+
});
|
|
1519
|
+
if (visibleOutput) return await deliverQuick(item, visibleOutput, signal);
|
|
1520
|
+
const target = {
|
|
1521
|
+
tabId
|
|
1522
|
+
};
|
|
1523
|
+
let attached = false, position = null;
|
|
1524
|
+
const network = watchNetwork(target);
|
|
1525
|
+
const ctx = makeContext("quick", tabId, item.url, options, target, signal);
|
|
1526
|
+
try {
|
|
1527
|
+
await debuggerAttach(target, "1.3");
|
|
1528
|
+
attached = true;
|
|
1529
|
+
if ((config.captureScope?.(options) || "full") === "full") position = await getScrollPosition(target);
|
|
1530
|
+
await config.beforeQuick?.(ctx, tab);
|
|
1531
|
+
await sendCommand(target, "Network.enable", {}, signal);
|
|
1532
|
+
if ((config.captureScope?.(options) || "full") === "full") {
|
|
1533
|
+
await preparePageForCapture(target, signal);
|
|
1534
|
+
}
|
|
1535
|
+
await suppressCaptureDecorations(target, signal);
|
|
1536
|
+
await prepareCapture(target, item, options, run, network, "quick");
|
|
1537
|
+
const output = await capturePrepared(ctx, options, run, network);
|
|
1538
|
+
throwIfAborted(signal);
|
|
1539
|
+
return await deliverQuick(item, output, signal);
|
|
1540
|
+
} finally {
|
|
1541
|
+
network.dispose();
|
|
1542
|
+
if (attached) {
|
|
1543
|
+
await config.afterQuick?.(ctx).catch(() => {
|
|
1544
|
+
});
|
|
1545
|
+
await restoreCaptureDecorations(target).catch(() => {
|
|
1546
|
+
});
|
|
1547
|
+
if (position && await isCurrentQuickPage(ctx)) await restorePageAfterQuick(target, position).catch(() => {
|
|
1548
|
+
});
|
|
1549
|
+
await debuggerDetach(target).catch(() => {
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
905
1552
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
await
|
|
909
|
-
await revealAnimations(target, true);
|
|
910
|
-
await delay(400);
|
|
911
|
-
const output = await config.capture(makeContext("quick", tabId, pageUrl, options, target));
|
|
912
|
-
const fileName = buildQuickFileName(config.brand, texts.quickFolder, pageUrl, output.extension);
|
|
913
|
-
await downloader.downloadViaBlob(output.base64, output.mime, fileName);
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
const saved = (await storageGet(QUICK_KEY))[QUICK_KEY];
|
|
1555
|
+
if (!saved?.downloadId && !saved?.downloadUrl) await storageRemove(QUICK_KEY);
|
|
914
1556
|
return {
|
|
915
|
-
|
|
916
|
-
|
|
1557
|
+
ok: false,
|
|
1558
|
+
error: normalizeError(error),
|
|
1559
|
+
quota: await getQuota(true)
|
|
917
1560
|
};
|
|
918
1561
|
} finally {
|
|
919
|
-
|
|
920
|
-
await restorePageAfterQuick(target, scrollPosition).catch(() => {
|
|
921
|
-
});
|
|
922
|
-
await debuggerDetach(target).catch(() => {
|
|
923
|
-
});
|
|
924
|
-
}
|
|
1562
|
+
releaseRun(run);
|
|
925
1563
|
}
|
|
926
1564
|
}
|
|
927
|
-
__name(
|
|
928
|
-
async function
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1565
|
+
__name(quickCapture, "quickCapture");
|
|
1566
|
+
async function deliverQuick(item, output, signal) {
|
|
1567
|
+
throwIfAborted(signal);
|
|
1568
|
+
item.warning = [
|
|
1569
|
+
item.warning,
|
|
1570
|
+
output.warning
|
|
1571
|
+
].filter(Boolean).join(" ");
|
|
1572
|
+
item.fileName = buildQuickFileName(config.brand, texts.quickFolder, item.url, output.extension);
|
|
1573
|
+
await saveOutput(item, output, signal, () => storageSet({
|
|
1574
|
+
[QUICK_KEY]: item
|
|
1575
|
+
}));
|
|
1576
|
+
await commitResult(null, item, "success", "", true);
|
|
934
1577
|
return {
|
|
935
1578
|
ok: true,
|
|
936
|
-
|
|
1579
|
+
result: {
|
|
1580
|
+
fileName: item.fileName,
|
|
1581
|
+
warning: item.warning
|
|
1582
|
+
},
|
|
1583
|
+
quota: await getQuota(true)
|
|
937
1584
|
};
|
|
938
1585
|
}
|
|
939
|
-
__name(
|
|
940
|
-
async function
|
|
941
|
-
if (
|
|
1586
|
+
__name(deliverQuick, "deliverQuick");
|
|
1587
|
+
async function isCurrentQuickPage(ctx) {
|
|
1588
|
+
if (ctx.mode !== "quick") return true;
|
|
1589
|
+
try {
|
|
1590
|
+
const current = await chrome.tabs.get(ctx.tabId);
|
|
1591
|
+
return current.url === ctx.url && (!current.pendingUrl || current.pendingUrl === ctx.url);
|
|
1592
|
+
} catch {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
__name(isCurrentQuickPage, "isCurrentQuickPage");
|
|
1597
|
+
async function assertCurrentQuickPage(ctx) {
|
|
1598
|
+
if (!await isCurrentQuickPage(ctx)) throw new Error("The page changed during capture. Return to the requested page and try again.");
|
|
1599
|
+
}
|
|
1600
|
+
__name(assertCurrentQuickPage, "assertCurrentQuickPage");
|
|
1601
|
+
async function capturePrepared(ctx, options, run, network) {
|
|
1602
|
+
const expand = (config.captureScope?.(options) || "full") === "full" && options.expandScrollAreas;
|
|
1603
|
+
try {
|
|
1604
|
+
await assertCurrentQuickPage(ctx);
|
|
1605
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
1606
|
+
if (expand) await scrollCapture?.prepare(ctx, async () => {
|
|
1607
|
+
await assertCurrentQuickPage(ctx);
|
|
1608
|
+
const warning = await waitForPageStable({
|
|
1609
|
+
tabId: ctx.tabId
|
|
1610
|
+
}, options, run.controller.signal, network);
|
|
1611
|
+
if (warning) warnings.add(warning);
|
|
1612
|
+
});
|
|
1613
|
+
await assertCurrentQuickPage(ctx);
|
|
1614
|
+
const output = await config.capture(ctx);
|
|
1615
|
+
await assertCurrentQuickPage(ctx);
|
|
1616
|
+
if (output.warning) warnings.add(output.warning);
|
|
942
1617
|
return {
|
|
943
|
-
|
|
944
|
-
|
|
1618
|
+
...output,
|
|
1619
|
+
warning: [
|
|
1620
|
+
...warnings
|
|
1621
|
+
].join(" ")
|
|
945
1622
|
};
|
|
1623
|
+
} finally {
|
|
1624
|
+
if (expand) await scrollCapture?.restore(ctx);
|
|
946
1625
|
}
|
|
947
|
-
batch = idleBatch();
|
|
948
|
-
await storageRemove(BATCH_KEY);
|
|
949
|
-
setBadge("");
|
|
950
|
-
return {
|
|
951
|
-
ok: true,
|
|
952
|
-
batch: publicBatch()
|
|
953
|
-
};
|
|
954
1626
|
}
|
|
955
|
-
__name(
|
|
956
|
-
async function
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
item.status = "running";
|
|
976
|
-
batch.current = item.url;
|
|
977
|
-
setBadge(`${batch.done + 1}/${batch.total}`);
|
|
978
|
-
await persistBatch();
|
|
979
|
-
let shouldCharge = false;
|
|
980
|
-
try {
|
|
981
|
-
const result = await captureUrl(item, options);
|
|
982
|
-
item.fileName = result.fileName;
|
|
983
|
-
item.warning = result.warning;
|
|
984
|
-
item.status = "success";
|
|
985
|
-
batch.success += 1;
|
|
986
|
-
if (!batch.pro && !item.usageRecorded) {
|
|
987
|
-
item.usageRecorded = true;
|
|
988
|
-
shouldCharge = true;
|
|
989
|
-
}
|
|
990
|
-
} catch (error) {
|
|
991
|
-
item.status = "failed";
|
|
992
|
-
item.error = normalizeError(error);
|
|
993
|
-
batch.failed += 1;
|
|
994
|
-
}
|
|
995
|
-
batch.done = batch.success + batch.failed;
|
|
996
|
-
await persistBatch();
|
|
997
|
-
if (shouldCharge) {
|
|
998
|
-
await recordUsage(1);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
batch.running = false;
|
|
1002
|
-
batch.current = batch.cancelRequested ? "Stopped" : "Done";
|
|
1003
|
-
batch.needVerify = null;
|
|
1004
|
-
await persistBatch();
|
|
1005
|
-
setBadge(batch.failed ? "ERR" : "OK");
|
|
1006
|
-
setTimeout(() => setBadge(""), 4e3);
|
|
1627
|
+
__name(capturePrepared, "capturePrepared");
|
|
1628
|
+
async function prepareCapture(target, item, options, run, network, mode = "batch") {
|
|
1629
|
+
const signal = run.controller.signal;
|
|
1630
|
+
const probe = await probePage(target, signal);
|
|
1631
|
+
if (probe.error) throw new Error(probe.error);
|
|
1632
|
+
if (probe.challenge && !options.ignoreChallenge) throw challengeError(probe.challenge);
|
|
1633
|
+
if (probe.challenge) item.warning = `Captured a ${probe.challenge} verification page at your request.`;
|
|
1634
|
+
const scope = config.captureScope?.(options) || "full";
|
|
1635
|
+
if (scope === "viewport" && mode === "quick") return;
|
|
1636
|
+
await delay(options.delayMs, signal);
|
|
1637
|
+
const before = await waitForPageStable(target, options, signal, network);
|
|
1638
|
+
const maxSeenY = scope === "full" ? await autoScroll(target, signal) : 0;
|
|
1639
|
+
const after = await waitForPageStable(target, options, signal, network, maxSeenY);
|
|
1640
|
+
item.warning = [
|
|
1641
|
+
...new Set([
|
|
1642
|
+
item.warning,
|
|
1643
|
+
before,
|
|
1644
|
+
after
|
|
1645
|
+
].filter(Boolean))
|
|
1646
|
+
].join(" ");
|
|
1007
1647
|
}
|
|
1008
|
-
__name(
|
|
1009
|
-
async function
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1648
|
+
__name(prepareCapture, "prepareCapture");
|
|
1649
|
+
async function saveOutput(item, output, signal, save) {
|
|
1650
|
+
throwIfAborted(signal);
|
|
1651
|
+
await downloader.downloadViaBlob(output.base64, output.mime, item.fileName, {
|
|
1652
|
+
signal,
|
|
1653
|
+
onPrepared: /* @__PURE__ */ __name(async (url) => {
|
|
1654
|
+
item.downloadUrl = url;
|
|
1655
|
+
item.status = "downloading";
|
|
1656
|
+
await save();
|
|
1657
|
+
}, "onPrepared"),
|
|
1658
|
+
onCreated: /* @__PURE__ */ __name(async (id) => {
|
|
1659
|
+
item.downloadId = id;
|
|
1660
|
+
await save();
|
|
1661
|
+
}, "onCreated")
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
__name(saveOutput, "saveOutput");
|
|
1665
|
+
async function captureUrl(item, job, run) {
|
|
1666
|
+
const signal = run.controller.signal;
|
|
1667
|
+
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
1668
|
+
throwIfAborted(signal);
|
|
1016
1669
|
try {
|
|
1017
|
-
return await captureOnce(item,
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if (attempt < maxChallengeRetry) {
|
|
1024
|
-
const backoff = 8e3 * (attempt + 1);
|
|
1025
|
-
batch.current = `Rate-limited \u2014 cooling down ${backoff / 1e3}s (retry ${attempt + 1}/${maxChallengeRetry})`;
|
|
1026
|
-
await delay(backoff);
|
|
1027
|
-
}
|
|
1670
|
+
return await captureOnce(item, job, run, attempt === 3);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
if (!isChallengeError(error) || attempt === 3) throw error;
|
|
1673
|
+
job.current = "Waiting before retrying verification\u2026";
|
|
1674
|
+
await persist(job);
|
|
1675
|
+
await delay(8e3 * (attempt + 1), signal);
|
|
1028
1676
|
}
|
|
1029
1677
|
}
|
|
1030
|
-
if (isChallengeError(lastError) && !batch.cancelRequested) {
|
|
1031
|
-
return await captureOnce(item, options, true);
|
|
1032
|
-
}
|
|
1033
|
-
throw lastError;
|
|
1034
1678
|
}
|
|
1035
1679
|
__name(captureUrl, "captureUrl");
|
|
1036
|
-
async function
|
|
1680
|
+
async function ensureBatchWindow(run) {
|
|
1681
|
+
if (Number.isInteger(run.windowId)) {
|
|
1682
|
+
const live = await chrome.windows.get(run.windowId).catch(() => null);
|
|
1683
|
+
if (live) return run.windowId;
|
|
1684
|
+
run.windowId = null;
|
|
1685
|
+
run.idleTabId = null;
|
|
1686
|
+
}
|
|
1687
|
+
const created = await chrome.windows.create({
|
|
1688
|
+
url: "about:blank",
|
|
1689
|
+
focused: false
|
|
1690
|
+
}).catch(() => null);
|
|
1691
|
+
run.windowId = Number.isInteger(created?.id) ? created.id : null;
|
|
1692
|
+
run.idleTabId = created?.tabs?.[0]?.id ?? null;
|
|
1693
|
+
await saveTabLease(run);
|
|
1694
|
+
return run.windowId;
|
|
1695
|
+
}
|
|
1696
|
+
__name(ensureBatchWindow, "ensureBatchWindow");
|
|
1697
|
+
function saveTabLease(run, tabId = null) {
|
|
1698
|
+
return storageCall("session", "set", {
|
|
1699
|
+
[TAB_KEY]: {
|
|
1700
|
+
tabId,
|
|
1701
|
+
jobId: run.job?.id,
|
|
1702
|
+
windowId: run.windowId,
|
|
1703
|
+
idleTabId: run.idleTabId
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
__name(saveTabLease, "saveTabLease");
|
|
1708
|
+
async function closeIdleTab(lease) {
|
|
1709
|
+
if (!Number.isInteger(lease?.idleTabId)) return;
|
|
1710
|
+
const tab = await chrome.tabs.get(lease.idleTabId).catch(() => null);
|
|
1711
|
+
if (tab?.windowId === lease.windowId && tab.url === "about:blank" && (!tab.pendingUrl || tab.pendingUrl === "about:blank")) await chrome.tabs.remove(tab.id).catch(() => {
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
__name(closeIdleTab, "closeIdleTab");
|
|
1715
|
+
async function closeBatchWindow(run) {
|
|
1716
|
+
await closeIdleTab(run);
|
|
1717
|
+
run.windowId = null;
|
|
1718
|
+
run.idleTabId = null;
|
|
1719
|
+
await storageCall("session", "remove", TAB_KEY).catch(() => {
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
__name(closeBatchWindow, "closeBatchWindow");
|
|
1723
|
+
async function captureOnce(item, job, run, interactive) {
|
|
1724
|
+
const signal = run.controller.signal, options = job.settings;
|
|
1725
|
+
const windowId = await ensureBatchWindow(run);
|
|
1037
1726
|
const tab = await chrome.tabs.create({
|
|
1038
|
-
url:
|
|
1039
|
-
active:
|
|
1727
|
+
url: "about:blank",
|
|
1728
|
+
active: interactive,
|
|
1729
|
+
...Number.isInteger(windowId) ? {
|
|
1730
|
+
windowId
|
|
1731
|
+
} : {}
|
|
1040
1732
|
});
|
|
1041
|
-
const tabId = tab.id
|
|
1042
|
-
if (!tabId) {
|
|
1043
|
-
throw new Error("Could not open tab");
|
|
1044
|
-
}
|
|
1045
|
-
const target = {
|
|
1733
|
+
const tabId = tab.id, target = {
|
|
1046
1734
|
tabId
|
|
1047
1735
|
};
|
|
1048
1736
|
let attached = false;
|
|
1737
|
+
const network = watchNetwork(target);
|
|
1049
1738
|
try {
|
|
1050
|
-
await
|
|
1051
|
-
|
|
1052
|
-
if (batch.cancelRequested) {
|
|
1053
|
-
throw new Error("Cancelled");
|
|
1054
|
-
}
|
|
1739
|
+
await saveTabLease(run, tabId);
|
|
1740
|
+
throwIfAborted(signal);
|
|
1055
1741
|
await debuggerAttach(target, "1.3");
|
|
1056
1742
|
attached = true;
|
|
1057
|
-
await sendCommand(target, "Page.enable");
|
|
1058
|
-
await sendCommand(target, "
|
|
1059
|
-
const
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1743
|
+
await sendCommand(target, "Page.enable", {}, signal);
|
|
1744
|
+
await sendCommand(target, "Network.enable", {}, signal);
|
|
1745
|
+
const ctx = makeContext("batch", tabId, item.url, options, target, signal);
|
|
1746
|
+
if (config.beforeScroll) await config.beforeScroll(ctx);
|
|
1747
|
+
await preparePageForCapture(target, signal);
|
|
1748
|
+
await chrome.tabs.update(tabId, {
|
|
1749
|
+
url: item.url
|
|
1750
|
+
});
|
|
1751
|
+
await waitForLoad(tabId, 3e4, signal);
|
|
1752
|
+
await suppressCaptureDecorations(target, signal);
|
|
1753
|
+
const probe = await probePage(target, signal);
|
|
1754
|
+
if (probe.challenge && !options.ignoreChallenge && interactive) {
|
|
1755
|
+
await debuggerDetach(target);
|
|
1069
1756
|
attached = false;
|
|
1070
1757
|
await focusTab(tabId);
|
|
1071
|
-
|
|
1758
|
+
job.needVerify = {
|
|
1072
1759
|
url: item.url,
|
|
1073
1760
|
challenge: probe.challenge
|
|
1074
1761
|
};
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const passed = await waitForVerification(tabId, () =>
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
await
|
|
1082
|
-
if (!passed) {
|
|
1083
|
-
throw challengeError(`${probe.challenge} verification not completed`);
|
|
1084
|
-
}
|
|
1085
|
-
await delay(800);
|
|
1762
|
+
job.current = `Complete ${probe.challenge} verification in the opened tab\u2026`;
|
|
1763
|
+
run.skipVerify = false;
|
|
1764
|
+
await persist(job);
|
|
1765
|
+
const passed = await waitForVerification(tabId, () => run.skipVerify, signal);
|
|
1766
|
+
job.needVerify = null;
|
|
1767
|
+
job.current = item.url;
|
|
1768
|
+
await persist(job);
|
|
1769
|
+
if (!passed) throw challengeError(`${probe.challenge} verification not completed`);
|
|
1086
1770
|
await debuggerAttach(target, "1.3");
|
|
1087
1771
|
attached = true;
|
|
1088
|
-
|
|
1089
|
-
await sendCommand(target, "
|
|
1772
|
+
network.reset();
|
|
1773
|
+
await sendCommand(target, "Network.enable", {}, signal);
|
|
1774
|
+
await preparePageForCapture(target, signal);
|
|
1090
1775
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
await downloader.downloadViaBlob(output.base64, output.mime, fileName);
|
|
1101
|
-
return {
|
|
1102
|
-
fileName,
|
|
1103
|
-
warning: output.warning || ""
|
|
1104
|
-
};
|
|
1776
|
+
await prepareCapture(target, item, options, run, network);
|
|
1777
|
+
const output = await capturePrepared(ctx, options, run, network);
|
|
1778
|
+
throwIfAborted(signal);
|
|
1779
|
+
item.warning = [
|
|
1780
|
+
item.warning,
|
|
1781
|
+
output.warning
|
|
1782
|
+
].filter(Boolean).join(" ");
|
|
1783
|
+
item.fileName = buildBatchFileName(job.folder, item, output.extension);
|
|
1784
|
+
await saveOutput(item, output, signal, () => persist(job));
|
|
1105
1785
|
} finally {
|
|
1786
|
+
network.dispose();
|
|
1106
1787
|
if (attached) {
|
|
1788
|
+
await restoreCaptureDecorations(target).catch(() => {
|
|
1789
|
+
});
|
|
1107
1790
|
await debuggerDetach(target).catch(() => {
|
|
1108
1791
|
});
|
|
1109
1792
|
}
|
|
1110
1793
|
await chrome.tabs.remove(tabId).catch(() => {
|
|
1111
1794
|
});
|
|
1795
|
+
await saveTabLease(run);
|
|
1112
1796
|
}
|
|
1113
1797
|
}
|
|
1114
1798
|
__name(captureOnce, "captureOnce");
|
|
1115
|
-
function makeContext(mode, tabId, url, options, target) {
|
|
1799
|
+
function makeContext(mode, tabId, url, options, target, signal) {
|
|
1116
1800
|
return {
|
|
1117
1801
|
mode,
|
|
1118
1802
|
tabId,
|
|
1119
1803
|
url,
|
|
1120
1804
|
options,
|
|
1121
|
-
send: /* @__PURE__ */ __name((method, params) => sendCommand(target, method, params), "send")
|
|
1805
|
+
send: /* @__PURE__ */ __name((method, params) => sendCommand(target, method, params, signal), "send"),
|
|
1806
|
+
cleanup: /* @__PURE__ */ __name((method, params) => sendCommand(target, method, params), "cleanup")
|
|
1122
1807
|
};
|
|
1123
1808
|
}
|
|
1124
1809
|
__name(makeContext, "makeContext");
|
|
1125
|
-
function publicBatch() {
|
|
1126
|
-
return {
|
|
1127
|
-
running: batch.running,
|
|
1128
|
-
total: batch.total,
|
|
1129
|
-
done: batch.done,
|
|
1130
|
-
success: batch.success,
|
|
1131
|
-
failed: batch.failed,
|
|
1132
|
-
current: batch.current,
|
|
1133
|
-
folder: batch.folder,
|
|
1134
|
-
freeLimited: batch.freeLimited,
|
|
1135
|
-
needVerify: batch.needVerify,
|
|
1136
|
-
items: batch.items.map((item) => ({
|
|
1137
|
-
url: item.url,
|
|
1138
|
-
index: item.index,
|
|
1139
|
-
status: item.status,
|
|
1140
|
-
fileName: item.fileName,
|
|
1141
|
-
error: item.error,
|
|
1142
|
-
warning: item.warning
|
|
1143
|
-
}))
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
__name(publicBatch, "publicBatch");
|
|
1147
|
-
async function persistBatch() {
|
|
1148
|
-
await storageSet({
|
|
1149
|
-
[BATCH_KEY]: batch
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
__name(persistBatch, "persistBatch");
|
|
1153
1810
|
async function restoreBatch() {
|
|
1154
|
-
const data = await storageGet(
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1811
|
+
const data = await storageGet([
|
|
1812
|
+
BATCH_KEY,
|
|
1813
|
+
QUICK_KEY
|
|
1814
|
+
]);
|
|
1815
|
+
const lease = (await storageCall("session", "get", TAB_KEY))[TAB_KEY];
|
|
1816
|
+
if (Number.isInteger(lease?.tabId) || Number.isInteger(lease?.windowId)) {
|
|
1817
|
+
if (Number.isInteger(lease.tabId)) await chrome.tabs.remove(lease.tabId).catch(() => {
|
|
1818
|
+
});
|
|
1819
|
+
await closeIdleTab(lease);
|
|
1820
|
+
await storageCall("session", "remove", TAB_KEY);
|
|
1158
1821
|
}
|
|
1822
|
+
await recoverQuick();
|
|
1823
|
+
const saved = data[BATCH_KEY];
|
|
1824
|
+
if (!Array.isArray(saved?.items) || !saved.items.length) return;
|
|
1825
|
+
const items = saved.items.filter((item) => typeof item?.url === "string" && normalizeUrls([
|
|
1826
|
+
item.url
|
|
1827
|
+
]).length).slice(0, 500);
|
|
1159
1828
|
batch = {
|
|
1160
1829
|
...idleBatch(),
|
|
1161
|
-
...saved
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1830
|
+
...saved,
|
|
1831
|
+
version: 2,
|
|
1832
|
+
settings: normalizeSettings(config.defaultSettings, config.migrateSettings ? config.migrateSettings(saved.settings) : saved.settings),
|
|
1833
|
+
needVerify: null,
|
|
1834
|
+
items: items.map((item, i) => ({
|
|
1835
|
+
...makeItem(item.url, i + 1),
|
|
1836
|
+
...item,
|
|
1837
|
+
chargeFree: item.chargeFree ?? !saved.pro,
|
|
1838
|
+
status: [
|
|
1839
|
+
"success",
|
|
1840
|
+
"failed",
|
|
1841
|
+
"cancelled",
|
|
1842
|
+
"downloading"
|
|
1843
|
+
].includes(item.status) ? item.status : "pending"
|
|
1844
|
+
}))
|
|
1166
1845
|
};
|
|
1167
|
-
batch.
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
batch.success = batch.items.filter((item) => item.status === "success").length;
|
|
1178
|
-
batch.failed = batch.items.filter((item) => item.status === "failed").length;
|
|
1179
|
-
batch.needVerify = null;
|
|
1180
|
-
batch.skipVerify = false;
|
|
1181
|
-
if (saved.running && batch.items.some((item) => item.status === "pending")) {
|
|
1182
|
-
batch.running = true;
|
|
1183
|
-
batch.cancelRequested = false;
|
|
1184
|
-
batch.current = "Resuming interrupted batch\u2026";
|
|
1185
|
-
await persistBatch();
|
|
1186
|
-
runBatch(batch.settings).catch(async (error) => {
|
|
1187
|
-
batch.running = false;
|
|
1188
|
-
batch.current = normalizeError(error);
|
|
1189
|
-
await persistBatch();
|
|
1190
|
-
});
|
|
1191
|
-
} else if (saved.running) {
|
|
1192
|
-
batch.running = false;
|
|
1193
|
-
batch.current = "Done";
|
|
1194
|
-
await persistBatch();
|
|
1846
|
+
batch.total = batch.items.length;
|
|
1847
|
+
if (!batch.folder) batch.folder = `${config.brand}/${folderNameFor(batch.items.map((i) => i.url))}`;
|
|
1848
|
+
recount(batch);
|
|
1849
|
+
if (saved.running) {
|
|
1850
|
+
const run = reserveRun(true);
|
|
1851
|
+
run.job = batch;
|
|
1852
|
+
if (saved.cancelRequested) run.controller.abort();
|
|
1853
|
+
batch.current = saved.cancelRequested ? "Stopping\u2026" : "Resuming interrupted batch\u2026";
|
|
1854
|
+
await persist(batch);
|
|
1855
|
+
void runBatch(batch, run);
|
|
1195
1856
|
}
|
|
1196
1857
|
}
|
|
1197
1858
|
__name(restoreBatch, "restoreBatch");
|
|
1859
|
+
async function handleMessage(message) {
|
|
1860
|
+
await restorePromise;
|
|
1861
|
+
switch (message?.type) {
|
|
1862
|
+
case `${prefix}_START`:
|
|
1863
|
+
return startBatch(message.urls, message.settings);
|
|
1864
|
+
case `${prefix}_CAPTURE_CURRENT`:
|
|
1865
|
+
case `${prefix}_CONVERT_CURRENT`:
|
|
1866
|
+
return quickCapture(message.tabId, message.settings);
|
|
1867
|
+
case `${prefix}_CANCEL`:
|
|
1868
|
+
if (activeRun) {
|
|
1869
|
+
activeRun.controller.abort();
|
|
1870
|
+
if (activeRun.job) {
|
|
1871
|
+
activeRun.job.cancelRequested = true;
|
|
1872
|
+
activeRun.job.current = "Stopping\u2026";
|
|
1873
|
+
await persist(activeRun.job);
|
|
1874
|
+
}
|
|
1875
|
+
} else if (batch.running) {
|
|
1876
|
+
const run = reserveRun(true);
|
|
1877
|
+
if (run) {
|
|
1878
|
+
run.job = batch;
|
|
1879
|
+
batch.cancelRequested = true;
|
|
1880
|
+
batch.current = "Stopping\u2026";
|
|
1881
|
+
run.controller.abort();
|
|
1882
|
+
await persist(batch).catch(() => {
|
|
1883
|
+
});
|
|
1884
|
+
void runBatch(batch, run);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
ok: true,
|
|
1889
|
+
batch: publicBatch()
|
|
1890
|
+
};
|
|
1891
|
+
case `${prefix}_CLEAR_BATCH`: {
|
|
1892
|
+
const run = reserveRun(true);
|
|
1893
|
+
if (!run) return {
|
|
1894
|
+
ok: false,
|
|
1895
|
+
error: "Stop the running task before clearing it"
|
|
1896
|
+
};
|
|
1897
|
+
try {
|
|
1898
|
+
run.job = batch;
|
|
1899
|
+
if (batch.running || batch.items.some((item) => item.status === "downloading")) {
|
|
1900
|
+
batch.cancelRequested = true;
|
|
1901
|
+
run.controller.abort();
|
|
1902
|
+
await runBatch(batch, run, {
|
|
1903
|
+
keepLock: true
|
|
1904
|
+
});
|
|
1905
|
+
if (batch.running) throw new Error("Downloads could not be confirmed. Results were kept; retry Stop or Clear.");
|
|
1906
|
+
}
|
|
1907
|
+
await storageRemove(BATCH_KEY);
|
|
1908
|
+
batch = idleBatch();
|
|
1909
|
+
} finally {
|
|
1910
|
+
releaseRun(run);
|
|
1911
|
+
}
|
|
1912
|
+
return {
|
|
1913
|
+
ok: true,
|
|
1914
|
+
batch: publicBatch(),
|
|
1915
|
+
quota: await getQuota(true)
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
case `${prefix}_SKIP_VERIFY`:
|
|
1919
|
+
if (activeRun) activeRun.skipVerify = true;
|
|
1920
|
+
return {
|
|
1921
|
+
ok: true
|
|
1922
|
+
};
|
|
1923
|
+
case `${prefix}_STATE`:
|
|
1924
|
+
return {
|
|
1925
|
+
ok: true,
|
|
1926
|
+
batch: publicBatch(),
|
|
1927
|
+
quota: await getQuota(true)
|
|
1928
|
+
};
|
|
1929
|
+
case `${prefix}_ACTIVATE`:
|
|
1930
|
+
await licensing.activate(message.code);
|
|
1931
|
+
return {
|
|
1932
|
+
ok: true,
|
|
1933
|
+
quota: await getQuota(true)
|
|
1934
|
+
};
|
|
1935
|
+
case `${prefix}_REFRESH_LICENSE`:
|
|
1936
|
+
return {
|
|
1937
|
+
ok: true,
|
|
1938
|
+
quota: await getQuota(false, true)
|
|
1939
|
+
};
|
|
1940
|
+
case `${prefix}_CLEAR_LICENSE`:
|
|
1941
|
+
await licensing.deactivate();
|
|
1942
|
+
return {
|
|
1943
|
+
ok: true,
|
|
1944
|
+
quota: await getQuota(true)
|
|
1945
|
+
};
|
|
1946
|
+
default:
|
|
1947
|
+
return {
|
|
1948
|
+
ok: false,
|
|
1949
|
+
error: "Unknown message"
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
__name(handleMessage, "handleMessage");
|
|
1954
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1955
|
+
if (message?.target === "offscreen" || sender.id && sender.id !== chrome.runtime.id) return false;
|
|
1956
|
+
handleMessage(message).then(sendResponse).catch((error) => sendResponse({
|
|
1957
|
+
ok: false,
|
|
1958
|
+
error: normalizeError(error)
|
|
1959
|
+
}));
|
|
1960
|
+
return true;
|
|
1961
|
+
});
|
|
1962
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1963
|
+
void restorePromise.catch(() => {
|
|
1964
|
+
});
|
|
1965
|
+
});
|
|
1198
1966
|
function setBadge(text) {
|
|
1199
1967
|
chrome.action.setBadgeText({
|
|
1200
1968
|
text
|
|
@@ -1204,10 +1972,11 @@ function initExtension(config) {
|
|
|
1204
1972
|
});
|
|
1205
1973
|
}
|
|
1206
1974
|
__name(setBadge, "setBadge");
|
|
1975
|
+
restorePromise = restoreBatch();
|
|
1207
1976
|
}
|
|
1208
1977
|
__name(initExtension, "initExtension");
|
|
1209
1978
|
|
|
1210
|
-
// src/popup.
|
|
1979
|
+
// src/popup.js
|
|
1211
1980
|
function initPopup(config) {
|
|
1212
1981
|
const byId = /* @__PURE__ */ __name((id) => document.getElementById(id), "byId");
|
|
1213
1982
|
const el = {
|
|
@@ -1243,20 +2012,66 @@ function initPopup(config) {
|
|
|
1243
2012
|
licenseCode: byId("licenseCode"),
|
|
1244
2013
|
activateBtn: byId("activateBtn"),
|
|
1245
2014
|
deactivateBtn: byId("deactivateBtn"),
|
|
2015
|
+
recheckLicense: byId("recheckLicense"),
|
|
2016
|
+
licenseHint: byId("licenseHint"),
|
|
1246
2017
|
status: byId("status")
|
|
1247
2018
|
};
|
|
1248
2019
|
let pollTimer = null;
|
|
2020
|
+
let disposed = false;
|
|
2021
|
+
window.addEventListener("pagehide", () => {
|
|
2022
|
+
disposed = true;
|
|
2023
|
+
pollGeneration++;
|
|
2024
|
+
clearTimeout(pollTimer);
|
|
2025
|
+
if (scanJob) {
|
|
2026
|
+
const job = scanJob;
|
|
2027
|
+
scanJob = null;
|
|
2028
|
+
void chrome.scripting.executeScript({
|
|
2029
|
+
target: {
|
|
2030
|
+
tabId: job.tabId
|
|
2031
|
+
},
|
|
2032
|
+
func: collectSameOriginLinks,
|
|
2033
|
+
args: [
|
|
2034
|
+
{
|
|
2035
|
+
phase: "cancel",
|
|
2036
|
+
id: job.id
|
|
2037
|
+
}
|
|
2038
|
+
]
|
|
2039
|
+
}).catch(() => {
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
}, {
|
|
2043
|
+
once: true
|
|
2044
|
+
});
|
|
1249
2045
|
let queue = [];
|
|
1250
2046
|
let queueLocked = false;
|
|
2047
|
+
let operationBusy = false;
|
|
2048
|
+
let editingIndex = -1;
|
|
2049
|
+
let scanJob = null;
|
|
2050
|
+
let pollGeneration = 0;
|
|
2051
|
+
const settingsKey = config.settingsKey || config.queueKey.replace(/_url_queue$/, "_settings");
|
|
2052
|
+
const settingFields = /* @__PURE__ */ __name(() => document.querySelectorAll(".settings select, .settings input[data-setting]"), "settingFields");
|
|
2053
|
+
let savedSettings = {};
|
|
1251
2054
|
const prefix = config.messagePrefix;
|
|
1252
|
-
void init();
|
|
2055
|
+
void init().catch((error) => setStatus(errText(error), true));
|
|
1253
2056
|
async function init() {
|
|
1254
2057
|
el.urls.addEventListener("input", updateUrlCount);
|
|
1255
2058
|
el.addUrls.addEventListener("click", addInputUrls);
|
|
1256
2059
|
el.scanSite.addEventListener("click", scanCurrentSite);
|
|
1257
2060
|
el.selectAll.addEventListener("click", () => setAllSelected(true));
|
|
1258
2061
|
el.selectNone.addEventListener("click", () => setAllSelected(false));
|
|
1259
|
-
el.moreQueueActions.addEventListener("click", () =>
|
|
2062
|
+
el.moreQueueActions.addEventListener("click", (event) => {
|
|
2063
|
+
event.stopPropagation();
|
|
2064
|
+
setQueueMenu(el.queueMenu.classList.contains("hidden"));
|
|
2065
|
+
});
|
|
2066
|
+
document.addEventListener("click", (event) => {
|
|
2067
|
+
if (!el.queueMenu.classList.contains("hidden") && !el.queueMenu.contains(event.target)) setQueueMenu(false);
|
|
2068
|
+
});
|
|
2069
|
+
document.addEventListener("keydown", (event) => {
|
|
2070
|
+
if (event.key === "Escape" && !el.queueMenu.classList.contains("hidden")) {
|
|
2071
|
+
setQueueMenu(false);
|
|
2072
|
+
el.moreQueueActions.focus();
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
1260
2075
|
el.removeUnselected.addEventListener("click", removeUnselected);
|
|
1261
2076
|
el.clearQueue.addEventListener("click", clearQueue);
|
|
1262
2077
|
el.captureCurrent.addEventListener("click", quickCapture);
|
|
@@ -1269,12 +2084,28 @@ function initPopup(config) {
|
|
|
1269
2084
|
el.haveCode.addEventListener("click", () => el.licensePanel.classList.toggle("hidden"));
|
|
1270
2085
|
el.activateBtn.addEventListener("click", activateLicense);
|
|
1271
2086
|
el.deactivateBtn.addEventListener("click", deactivateLicense);
|
|
1272
|
-
|
|
2087
|
+
el.recheckLicense?.addEventListener("click", recheckLicense);
|
|
2088
|
+
el.start.disabled = true;
|
|
2089
|
+
el.captureCurrent.disabled = true;
|
|
2090
|
+
await Promise.all([
|
|
2091
|
+
loadQueue(),
|
|
2092
|
+
loadSettings()
|
|
2093
|
+
]);
|
|
2094
|
+
config.onSettingsLoaded?.();
|
|
2095
|
+
settingFields().forEach((field) => {
|
|
2096
|
+
field.addEventListener("change", saveSettings);
|
|
2097
|
+
if (field.type === "number") field.addEventListener("input", saveSettings);
|
|
2098
|
+
});
|
|
1273
2099
|
updateUrlCount();
|
|
1274
2100
|
renderQueue();
|
|
1275
2101
|
await refreshState();
|
|
1276
2102
|
}
|
|
1277
2103
|
__name(init, "init");
|
|
2104
|
+
function setQueueMenu(open) {
|
|
2105
|
+
el.queueMenu.classList.toggle("hidden", !open);
|
|
2106
|
+
el.moreQueueActions.setAttribute("aria-expanded", String(open));
|
|
2107
|
+
}
|
|
2108
|
+
__name(setQueueMenu, "setQueueMenu");
|
|
1278
2109
|
async function activateLicense() {
|
|
1279
2110
|
const code = el.licenseCode.value.trim();
|
|
1280
2111
|
if (!code) {
|
|
@@ -1299,15 +2130,29 @@ function initPopup(config) {
|
|
|
1299
2130
|
}
|
|
1300
2131
|
__name(activateLicense, "activateLicense");
|
|
1301
2132
|
async function deactivateLicense() {
|
|
2133
|
+
el.deactivateBtn.disabled = true;
|
|
1302
2134
|
const response = await send({
|
|
1303
2135
|
type: `${prefix}_CLEAR_LICENSE`
|
|
1304
2136
|
});
|
|
2137
|
+
el.deactivateBtn.disabled = false;
|
|
1305
2138
|
if (response.ok) {
|
|
1306
2139
|
renderQuota(response.quota);
|
|
1307
|
-
setStatus("
|
|
1308
|
-
}
|
|
2140
|
+
setStatus("Device released. Its license slot is now available.");
|
|
2141
|
+
} else setStatus(response.error || "Could not release this device", true);
|
|
1309
2142
|
}
|
|
1310
2143
|
__name(deactivateLicense, "deactivateLicense");
|
|
2144
|
+
async function recheckLicense() {
|
|
2145
|
+
el.recheckLicense.disabled = true;
|
|
2146
|
+
setStatus("Checking your Pro license\u2026");
|
|
2147
|
+
const response = await send({
|
|
2148
|
+
type: `${prefix}_REFRESH_LICENSE`
|
|
2149
|
+
});
|
|
2150
|
+
el.recheckLicense.disabled = false;
|
|
2151
|
+
if (!response.ok) return setStatus(response.error || "Could not check your license", true);
|
|
2152
|
+
renderQuota(response.quota);
|
|
2153
|
+
setStatus(response.quota.pro ? "Pro confirmed \u2014 unlimited exports." : response.quota.licenseMessage || "Enter your original activation code to restore Pro in this extension.", !response.quota.pro);
|
|
2154
|
+
}
|
|
2155
|
+
__name(recheckLicense, "recheckLicense");
|
|
1311
2156
|
function parseUrls() {
|
|
1312
2157
|
return normalizeInputUrls(el.urls.value.split(/\s+/));
|
|
1313
2158
|
}
|
|
@@ -1325,7 +2170,6 @@ function initPopup(config) {
|
|
|
1325
2170
|
if (!/^https?:$/.test(url.protocol)) {
|
|
1326
2171
|
continue;
|
|
1327
2172
|
}
|
|
1328
|
-
url.hash = "";
|
|
1329
2173
|
const normalized = url.toString();
|
|
1330
2174
|
if (!seen.has(normalized)) {
|
|
1331
2175
|
seen.add(normalized);
|
|
@@ -1370,35 +2214,80 @@ function initPopup(config) {
|
|
|
1370
2214
|
}
|
|
1371
2215
|
__name(addUrlsToQueue, "addUrlsToQueue");
|
|
1372
2216
|
async function scanCurrentSite() {
|
|
1373
|
-
|
|
2217
|
+
if (scanJob) {
|
|
2218
|
+
const stopped = scanJob;
|
|
2219
|
+
scanJob = null;
|
|
2220
|
+
await chrome.scripting.executeScript({
|
|
2221
|
+
target: {
|
|
2222
|
+
tabId: stopped.tabId
|
|
2223
|
+
},
|
|
2224
|
+
func: collectSameOriginLinks,
|
|
2225
|
+
args: [
|
|
2226
|
+
{
|
|
2227
|
+
phase: "cancel",
|
|
2228
|
+
id: stopped.id
|
|
2229
|
+
}
|
|
2230
|
+
]
|
|
2231
|
+
}).catch(() => {
|
|
2232
|
+
});
|
|
2233
|
+
el.scanSite.textContent = "\u2316 Scan site";
|
|
2234
|
+
setStatus("Scan stopped; imported links are kept");
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if (queueLocked || operationBusy) return;
|
|
2238
|
+
let job;
|
|
1374
2239
|
try {
|
|
1375
2240
|
const [tab] = await chrome.tabs.query({
|
|
1376
2241
|
active: true,
|
|
1377
2242
|
currentWindow: true
|
|
1378
2243
|
});
|
|
1379
|
-
if (!tab?.id)
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
2244
|
+
if (!tab?.id || !/^https?:\/\//i.test(tab.url || "")) throw new Error("Open an http(s) website first");
|
|
2245
|
+
job = {
|
|
2246
|
+
tabId: tab.id,
|
|
2247
|
+
id: crypto.randomUUID()
|
|
2248
|
+
};
|
|
2249
|
+
scanJob = job;
|
|
2250
|
+
el.scanSite.textContent = "Stop scan";
|
|
2251
|
+
const [links] = await chrome.scripting.executeScript({
|
|
1383
2252
|
target: {
|
|
1384
2253
|
tabId: tab.id
|
|
1385
2254
|
},
|
|
1386
|
-
func: collectSameOriginLinks
|
|
2255
|
+
func: collectSameOriginLinks,
|
|
2256
|
+
args: [
|
|
2257
|
+
{
|
|
2258
|
+
phase: "links"
|
|
2259
|
+
}
|
|
2260
|
+
]
|
|
1387
2261
|
});
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
2262
|
+
if (scanJob !== job) return;
|
|
2263
|
+
const added = addUrlsToQueue(links?.result?.urls || []);
|
|
2264
|
+
setStatus(`Added ${added} page links; checking sitemap\u2026`);
|
|
2265
|
+
const [maps] = await chrome.scripting.executeScript({
|
|
2266
|
+
target: {
|
|
2267
|
+
tabId: tab.id
|
|
2268
|
+
},
|
|
2269
|
+
func: collectSameOriginLinks,
|
|
2270
|
+
args: [
|
|
2271
|
+
{
|
|
2272
|
+
phase: "sitemap",
|
|
2273
|
+
id: job.id
|
|
2274
|
+
}
|
|
2275
|
+
]
|
|
2276
|
+
});
|
|
2277
|
+
if (scanJob !== job) return;
|
|
2278
|
+
if (queueLocked || operationBusy) {
|
|
2279
|
+
setStatus("Scan stopped when the task started; page links are kept");
|
|
1395
2280
|
return;
|
|
1396
2281
|
}
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
setStatus(`Found ${found.length} same-site URLs${sitemapNote}; added ${added} new`);
|
|
2282
|
+
const extra = addUrlsToQueue(maps?.result?.urls || []);
|
|
2283
|
+
setStatus(`Added ${added + extra} URLs${maps?.result?.incomplete ? "; some sitemaps could not be read" : ""}`);
|
|
1400
2284
|
} catch (error) {
|
|
1401
2285
|
setStatus(errText(error), true);
|
|
2286
|
+
} finally {
|
|
2287
|
+
if (scanJob === job) {
|
|
2288
|
+
scanJob = null;
|
|
2289
|
+
el.scanSite.textContent = "\u2316 Scan site";
|
|
2290
|
+
}
|
|
1402
2291
|
}
|
|
1403
2292
|
}
|
|
1404
2293
|
__name(scanCurrentSite, "scanCurrentSite");
|
|
@@ -1408,14 +2297,24 @@ function initPopup(config) {
|
|
|
1408
2297
|
setStatus("Select at least one URL from the queue", true);
|
|
1409
2298
|
return;
|
|
1410
2299
|
}
|
|
1411
|
-
|
|
2300
|
+
operationBusy = true;
|
|
2301
|
+
renderQueue();
|
|
1412
2302
|
setStatus("Starting\u2026");
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2303
|
+
let response;
|
|
2304
|
+
try {
|
|
2305
|
+
response = await send({
|
|
2306
|
+
type: `${prefix}_START`,
|
|
2307
|
+
urls,
|
|
2308
|
+
settings: await config.collectSettings()
|
|
2309
|
+
});
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
response = {
|
|
2312
|
+
ok: false,
|
|
2313
|
+
error: errText(error)
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
operationBusy = false;
|
|
2317
|
+
renderQueue();
|
|
1419
2318
|
if (!response.ok) {
|
|
1420
2319
|
setStatus(response.error || "Failed to start", true);
|
|
1421
2320
|
return;
|
|
@@ -1435,15 +2334,25 @@ function initPopup(config) {
|
|
|
1435
2334
|
setStatus("Open an http(s) website first", true);
|
|
1436
2335
|
return;
|
|
1437
2336
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
2337
|
+
operationBusy = true;
|
|
2338
|
+
renderQueue();
|
|
2339
|
+
el.cancel.classList.remove("hidden");
|
|
1440
2340
|
setStatus(config.texts.quickIng);
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
2341
|
+
let response;
|
|
2342
|
+
try {
|
|
2343
|
+
response = await send({
|
|
2344
|
+
type: `${prefix}_${config.quickType}`,
|
|
2345
|
+
tabId: tab.id,
|
|
2346
|
+
settings: await config.collectQuickSettings()
|
|
2347
|
+
});
|
|
2348
|
+
} catch (error) {
|
|
2349
|
+
response = {
|
|
2350
|
+
ok: false,
|
|
2351
|
+
error: errText(error)
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
operationBusy = false;
|
|
2355
|
+
await refreshState();
|
|
1447
2356
|
renderQueue();
|
|
1448
2357
|
if (!response.ok || !response.result) {
|
|
1449
2358
|
setStatus(response.error || config.texts.quickFailed, true);
|
|
@@ -1472,6 +2381,7 @@ function initPopup(config) {
|
|
|
1472
2381
|
}
|
|
1473
2382
|
renderBatch(response.batch);
|
|
1474
2383
|
setStatus("Batch results cleared");
|
|
2384
|
+
renderQuota(response.quota);
|
|
1475
2385
|
}
|
|
1476
2386
|
__name(clearBatchResults, "clearBatchResults");
|
|
1477
2387
|
async function refreshState() {
|
|
@@ -1483,45 +2393,46 @@ function initPopup(config) {
|
|
|
1483
2393
|
}
|
|
1484
2394
|
renderQuota(response.quota);
|
|
1485
2395
|
renderBatch(response.batch);
|
|
1486
|
-
if (response.batch?.running) {
|
|
2396
|
+
if (response.batch?.running || response.batch?.busy || response.quota?.licenseRefreshing) {
|
|
1487
2397
|
schedulePoll();
|
|
1488
2398
|
}
|
|
1489
2399
|
}
|
|
1490
2400
|
__name(refreshState, "refreshState");
|
|
1491
2401
|
function schedulePoll() {
|
|
1492
|
-
if (
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
2402
|
+
if (disposed) return;
|
|
2403
|
+
clearTimeout(pollTimer);
|
|
2404
|
+
const generation = ++pollGeneration;
|
|
2405
|
+
async function poll() {
|
|
1496
2406
|
const response = await send({
|
|
1497
2407
|
type: `${prefix}_STATE`
|
|
1498
2408
|
});
|
|
1499
|
-
if (
|
|
1500
|
-
|
|
2409
|
+
if (disposed || generation !== pollGeneration) return;
|
|
2410
|
+
if (response.ok) {
|
|
2411
|
+
renderQuota(response.quota);
|
|
2412
|
+
renderBatch(response.batch);
|
|
1501
2413
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
}, 800);
|
|
2414
|
+
if (!response.ok || response.batch?.running || response.batch?.busy || response.quota?.licenseRefreshing) pollTimer = setTimeout(poll, 800);
|
|
2415
|
+
}
|
|
2416
|
+
__name(poll, "poll");
|
|
2417
|
+
pollTimer = setTimeout(poll, 800);
|
|
1508
2418
|
}
|
|
1509
2419
|
__name(schedulePoll, "schedulePoll");
|
|
1510
2420
|
function renderQuota(quota) {
|
|
1511
2421
|
if (!quota) {
|
|
1512
2422
|
return;
|
|
1513
2423
|
}
|
|
2424
|
+
if (el.licenseHint) 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.");
|
|
2425
|
+
el.recheckLicense?.classList.toggle("hidden", !quota.hasLicense);
|
|
2426
|
+
el.proLink.classList.toggle("hidden", quota.pro || quota.hasLicense);
|
|
2427
|
+
el.deactivateBtn.classList.toggle("hidden", !quota.hasLicense);
|
|
2428
|
+
el.activateBtn.classList.toggle("hidden", quota.pro);
|
|
1514
2429
|
if (quota.pro) {
|
|
1515
2430
|
const until = quota.expiresAt ? ` \xB7 ${String(quota.expiresAt).slice(0, 10)}` : "";
|
|
1516
2431
|
el.quotaText.textContent = `unlimited${until}`;
|
|
1517
|
-
|
|
1518
|
-
el.
|
|
1519
|
-
el.activateBtn.classList.add("hidden");
|
|
2432
|
+
} else if (quota.hasLicense) {
|
|
2433
|
+
el.quotaText.textContent = quota.licenseStatus === "checking" ? "Checking Pro\u2026" : "Pro needs verification";
|
|
1520
2434
|
} else {
|
|
1521
2435
|
el.quotaText.textContent = `${quota.remaining}/${quota.limit} today`;
|
|
1522
|
-
el.proLink.classList.remove("hidden");
|
|
1523
|
-
el.deactivateBtn.classList.add("hidden");
|
|
1524
|
-
el.activateBtn.classList.remove("hidden");
|
|
1525
2436
|
}
|
|
1526
2437
|
}
|
|
1527
2438
|
__name(renderQuota, "renderQuota");
|
|
@@ -1529,13 +2440,15 @@ function initPopup(config) {
|
|
|
1529
2440
|
if (!batch) {
|
|
1530
2441
|
return;
|
|
1531
2442
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
el.
|
|
1535
|
-
|
|
1536
|
-
|
|
2443
|
+
const busy = batch.running || batch.busy;
|
|
2444
|
+
const stalled = Boolean(batch.stalled);
|
|
2445
|
+
el.start.classList.toggle("hidden", busy);
|
|
2446
|
+
el.captureCurrent.classList.toggle("hidden", busy);
|
|
2447
|
+
el.cancel.classList.toggle("hidden", !busy);
|
|
2448
|
+
queueLocked = busy && !stalled;
|
|
2449
|
+
renderQueue(true);
|
|
1537
2450
|
el.progressWrap.classList.toggle("hidden", !batch.total);
|
|
1538
|
-
el.clearBatch.classList.toggle("hidden",
|
|
2451
|
+
el.clearBatch.classList.toggle("hidden", busy && !stalled || !batch.total);
|
|
1539
2452
|
if (batch.total) {
|
|
1540
2453
|
el.progressText.textContent = `${batch.done} / ${batch.total}`;
|
|
1541
2454
|
el.progressCurrent.textContent = batch.current || "";
|
|
@@ -1555,6 +2468,8 @@ function initPopup(config) {
|
|
|
1555
2468
|
state.textContent = {
|
|
1556
2469
|
pending: "\xB7",
|
|
1557
2470
|
running: "\u2026",
|
|
2471
|
+
downloading: "\u2193",
|
|
2472
|
+
cancelled: "\u2013",
|
|
1558
2473
|
success: "\u2713",
|
|
1559
2474
|
failed: "\u2717"
|
|
1560
2475
|
}[item.status] || "\xB7";
|
|
@@ -1576,7 +2491,7 @@ function initPopup(config) {
|
|
|
1576
2491
|
}
|
|
1577
2492
|
el.resultList.append(rowEl);
|
|
1578
2493
|
}
|
|
1579
|
-
if (batch.folder && batch.
|
|
2494
|
+
if (batch.folder && batch.success > 0) {
|
|
1580
2495
|
el.folderNote.classList.remove("hidden");
|
|
1581
2496
|
el.folderNote.textContent = `Saved to Downloads/${batch.folder}/`;
|
|
1582
2497
|
} else {
|
|
@@ -1585,6 +2500,35 @@ function initPopup(config) {
|
|
|
1585
2500
|
}
|
|
1586
2501
|
}
|
|
1587
2502
|
__name(renderBatch, "renderBatch");
|
|
2503
|
+
async function loadSettings() {
|
|
2504
|
+
const data = await chrome.storage.local.get(settingsKey);
|
|
2505
|
+
const saved = data[settingsKey];
|
|
2506
|
+
if (!saved || typeof saved !== "object") return;
|
|
2507
|
+
settingFields().forEach((field) => {
|
|
2508
|
+
const value = String(saved[field.id]);
|
|
2509
|
+
if (field.tagName === "SELECT" && [
|
|
2510
|
+
...field.options
|
|
2511
|
+
].some((option) => option.value === value)) field.value = value;
|
|
2512
|
+
if (field.type === "number" && value.trim() && Number.isFinite(Number(value))) {
|
|
2513
|
+
const fallback = field.value;
|
|
2514
|
+
field.value = value;
|
|
2515
|
+
if (!field.checkValidity()) field.value = fallback;
|
|
2516
|
+
}
|
|
2517
|
+
savedSettings[field.id] = field.value;
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
__name(loadSettings, "loadSettings");
|
|
2521
|
+
function saveSettings() {
|
|
2522
|
+
settingFields().forEach((field) => {
|
|
2523
|
+
if (field.type !== "number" || field.value.trim() && Number.isFinite(field.valueAsNumber) && field.checkValidity()) savedSettings[field.id] = field.value;
|
|
2524
|
+
});
|
|
2525
|
+
void chrome.storage.local.set({
|
|
2526
|
+
[settingsKey]: {
|
|
2527
|
+
...savedSettings
|
|
2528
|
+
}
|
|
2529
|
+
}).catch((error) => setStatus(errText(error), true));
|
|
2530
|
+
}
|
|
2531
|
+
__name(saveSettings, "saveSettings");
|
|
1588
2532
|
async function loadQueue() {
|
|
1589
2533
|
const data = await chrome.storage.local.get(config.queueKey);
|
|
1590
2534
|
const saved = Array.isArray(data[config.queueKey]) ? data[config.queueKey] : [];
|
|
@@ -1597,20 +2541,23 @@ function initPopup(config) {
|
|
|
1597
2541
|
function saveQueue() {
|
|
1598
2542
|
void chrome.storage.local.set({
|
|
1599
2543
|
[config.queueKey]: queue
|
|
1600
|
-
});
|
|
2544
|
+
}).catch((error) => setStatus(errText(error), true));
|
|
1601
2545
|
}
|
|
1602
2546
|
__name(saveQueue, "saveQueue");
|
|
1603
|
-
function renderQueue() {
|
|
2547
|
+
function renderQueue(preserveEditor = false) {
|
|
2548
|
+
if (queueLocked || operationBusy) editingIndex = -1;
|
|
2549
|
+
if (editingIndex >= queue.length) editingIndex = -1;
|
|
1604
2550
|
const selected = queue.filter((item) => item.selected).length;
|
|
1605
2551
|
el.queueSummary.textContent = `${selected}/${queue.length} selected`;
|
|
1606
2552
|
el.start.textContent = selected ? config.texts.startSome(selected) : config.texts.startNone;
|
|
1607
|
-
el.start.disabled = selected === 0;
|
|
1608
|
-
el.selectAll.disabled = queueLocked || !queue.length;
|
|
1609
|
-
el.selectNone.disabled = queueLocked || !queue.length;
|
|
1610
|
-
el.moreQueueActions.disabled = queueLocked || !queue.length;
|
|
1611
|
-
el.addUrls.disabled = queueLocked;
|
|
1612
|
-
el.scanSite.disabled = queueLocked;
|
|
1613
|
-
el.captureCurrent.disabled = queueLocked;
|
|
2553
|
+
el.start.disabled = operationBusy || queueLocked || selected === 0;
|
|
2554
|
+
el.selectAll.disabled = queueLocked || operationBusy || !queue.length;
|
|
2555
|
+
el.selectNone.disabled = queueLocked || operationBusy || !queue.length;
|
|
2556
|
+
el.moreQueueActions.disabled = queueLocked || operationBusy || !queue.length;
|
|
2557
|
+
el.addUrls.disabled = queueLocked || operationBusy;
|
|
2558
|
+
el.scanSite.disabled = queueLocked || operationBusy;
|
|
2559
|
+
el.captureCurrent.disabled = queueLocked || operationBusy;
|
|
2560
|
+
if (preserveEditor && editingIndex >= 0 && el.queueList.querySelector(".queueEdit")) return;
|
|
1614
2561
|
el.queueList.replaceChildren();
|
|
1615
2562
|
if (!queue.length) {
|
|
1616
2563
|
const empty = document.createElement("p");
|
|
@@ -1625,28 +2572,23 @@ function initPopup(config) {
|
|
|
1625
2572
|
const checkbox = document.createElement("input");
|
|
1626
2573
|
checkbox.type = "checkbox";
|
|
1627
2574
|
checkbox.checked = item.selected;
|
|
1628
|
-
checkbox.disabled = queueLocked;
|
|
2575
|
+
checkbox.disabled = queueLocked || operationBusy;
|
|
1629
2576
|
checkbox.setAttribute("aria-label", `Include ${item.url}`);
|
|
1630
2577
|
checkbox.addEventListener("change", () => {
|
|
1631
2578
|
item.selected = checkbox.checked;
|
|
1632
2579
|
saveQueue();
|
|
1633
2580
|
renderQueue();
|
|
1634
2581
|
});
|
|
1635
|
-
const url =
|
|
1636
|
-
url.type = "button";
|
|
1637
|
-
url.className = "queueUrl";
|
|
1638
|
-
url.textContent = item.url.replace(/^https?:\/\//, "");
|
|
1639
|
-
url.title = item.url;
|
|
1640
|
-
url.disabled = queueLocked;
|
|
1641
|
-
url.addEventListener("click", () => editQueueItem(index));
|
|
2582
|
+
const url = index === editingIndex ? buildQueueEditor(item, index) : buildQueueLabel(item, index);
|
|
1642
2583
|
const remove = document.createElement("button");
|
|
1643
2584
|
remove.type = "button";
|
|
1644
2585
|
remove.className = "queueRemove";
|
|
1645
2586
|
remove.textContent = "\xD7";
|
|
1646
2587
|
remove.title = "Remove URL";
|
|
1647
|
-
remove.disabled = queueLocked;
|
|
2588
|
+
remove.disabled = queueLocked || operationBusy;
|
|
1648
2589
|
remove.addEventListener("click", () => {
|
|
1649
2590
|
queue.splice(index, 1);
|
|
2591
|
+
editingIndex = -1;
|
|
1650
2592
|
saveQueue();
|
|
1651
2593
|
renderQueue();
|
|
1652
2594
|
});
|
|
@@ -1655,30 +2597,82 @@ function initPopup(config) {
|
|
|
1655
2597
|
});
|
|
1656
2598
|
}
|
|
1657
2599
|
__name(renderQueue, "renderQueue");
|
|
1658
|
-
function
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2600
|
+
function buildQueueLabel(item, index) {
|
|
2601
|
+
const url = document.createElement("button");
|
|
2602
|
+
url.type = "button";
|
|
2603
|
+
url.className = "queueUrl";
|
|
2604
|
+
url.textContent = item.url.replace(/^https?:\/\//, "");
|
|
2605
|
+
url.title = `${item.url}
|
|
2606
|
+
Click to edit`;
|
|
2607
|
+
url.disabled = queueLocked || operationBusy;
|
|
2608
|
+
url.addEventListener("click", () => {
|
|
2609
|
+
if (queueLocked || operationBusy) return;
|
|
2610
|
+
editingIndex = index;
|
|
2611
|
+
renderQueue();
|
|
2612
|
+
});
|
|
2613
|
+
return url;
|
|
2614
|
+
}
|
|
2615
|
+
__name(buildQueueLabel, "buildQueueLabel");
|
|
2616
|
+
function buildQueueEditor(item, index) {
|
|
2617
|
+
const input = document.createElement("input");
|
|
2618
|
+
input.type = "text";
|
|
2619
|
+
input.className = "queueEdit";
|
|
2620
|
+
input.value = item.url;
|
|
2621
|
+
input.spellcheck = false;
|
|
2622
|
+
input.setAttribute("aria-label", `Edit ${item.url}`);
|
|
2623
|
+
const leaveEditor = /* @__PURE__ */ __name(() => {
|
|
2624
|
+
editingIndex = -1;
|
|
2625
|
+
const label = buildQueueLabel(item, index);
|
|
2626
|
+
input.parentElement?.querySelector('input[type="checkbox"]')?.setAttribute("aria-label", `Include ${item.url}`);
|
|
2627
|
+
input.replaceWith(label);
|
|
2628
|
+
return label;
|
|
2629
|
+
}, "leaveEditor");
|
|
2630
|
+
const finish = /* @__PURE__ */ __name((fromBlur) => {
|
|
2631
|
+
if (editingIndex !== index) return;
|
|
2632
|
+
const [normalized] = normalizeInputUrls([
|
|
2633
|
+
input.value
|
|
2634
|
+
]);
|
|
2635
|
+
const duplicate = normalized && queue.some((other, otherIndex) => otherIndex !== index && other.url === normalized);
|
|
2636
|
+
if (!normalized || duplicate) {
|
|
2637
|
+
const message = duplicate ? "That URL is already in the queue" : "Enter a valid http(s) URL";
|
|
2638
|
+
if (fromBlur) {
|
|
2639
|
+
setStatus(`${message} \u2014 edit discarded`, true);
|
|
2640
|
+
leaveEditor();
|
|
2641
|
+
} else {
|
|
2642
|
+
setStatus(message, true);
|
|
2643
|
+
input.focus();
|
|
2644
|
+
input.select();
|
|
2645
|
+
}
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
if (normalized !== item.url) {
|
|
2649
|
+
item.url = normalized;
|
|
2650
|
+
saveQueue();
|
|
2651
|
+
setStatus("URL updated");
|
|
2652
|
+
}
|
|
2653
|
+
const label = leaveEditor();
|
|
2654
|
+
if (!fromBlur) label.focus();
|
|
2655
|
+
}, "finish");
|
|
2656
|
+
input.addEventListener("keydown", (event) => {
|
|
2657
|
+
if (event.key === "Enter") {
|
|
2658
|
+
event.preventDefault();
|
|
2659
|
+
finish(false);
|
|
2660
|
+
} else if (event.key === "Escape") {
|
|
2661
|
+
event.preventDefault();
|
|
2662
|
+
event.stopPropagation();
|
|
2663
|
+
leaveEditor().focus();
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
input.addEventListener("blur", () => finish(true));
|
|
2667
|
+
queueMicrotask(() => {
|
|
2668
|
+
input.focus();
|
|
2669
|
+
input.select();
|
|
2670
|
+
});
|
|
2671
|
+
return input;
|
|
1678
2672
|
}
|
|
1679
|
-
__name(
|
|
2673
|
+
__name(buildQueueEditor, "buildQueueEditor");
|
|
1680
2674
|
function setAllSelected(selected) {
|
|
1681
|
-
if (queueLocked) {
|
|
2675
|
+
if (queueLocked || operationBusy) {
|
|
1682
2676
|
return;
|
|
1683
2677
|
}
|
|
1684
2678
|
queue.forEach((item) => {
|
|
@@ -1689,15 +2683,19 @@ function initPopup(config) {
|
|
|
1689
2683
|
}
|
|
1690
2684
|
__name(setAllSelected, "setAllSelected");
|
|
1691
2685
|
function removeUnselected() {
|
|
2686
|
+
if (queueLocked || operationBusy) return;
|
|
1692
2687
|
queue = queue.filter((item) => item.selected);
|
|
1693
|
-
|
|
2688
|
+
editingIndex = -1;
|
|
2689
|
+
setQueueMenu(false);
|
|
1694
2690
|
saveQueue();
|
|
1695
2691
|
renderQueue();
|
|
1696
2692
|
}
|
|
1697
2693
|
__name(removeUnselected, "removeUnselected");
|
|
1698
2694
|
function clearQueue() {
|
|
2695
|
+
if (queueLocked || operationBusy) return;
|
|
1699
2696
|
queue = [];
|
|
1700
|
-
|
|
2697
|
+
editingIndex = -1;
|
|
2698
|
+
setQueueMenu(false);
|
|
1701
2699
|
saveQueue();
|
|
1702
2700
|
renderQueue();
|
|
1703
2701
|
}
|
|
@@ -1718,6 +2716,7 @@ function initPopup(config) {
|
|
|
1718
2716
|
}
|
|
1719
2717
|
__name(send, "send");
|
|
1720
2718
|
function setStatus(text, isError = false) {
|
|
2719
|
+
if (disposed) return;
|
|
1721
2720
|
el.status.textContent = text;
|
|
1722
2721
|
el.status.classList.toggle("error", isError);
|
|
1723
2722
|
}
|
|
@@ -1728,91 +2727,211 @@ function initPopup(config) {
|
|
|
1728
2727
|
__name(errText, "errText");
|
|
1729
2728
|
}
|
|
1730
2729
|
__name(initPopup, "initPopup");
|
|
1731
|
-
async function collectSameOriginLinks() {
|
|
1732
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1733
|
-
const out = [];
|
|
1734
|
-
const origin = location.origin;
|
|
1735
|
-
let sitemapCount = 0;
|
|
2730
|
+
async function collectSameOriginLinks(options = {}) {
|
|
2731
|
+
const origin = location.origin, seen = /* @__PURE__ */ new Set(), urls = [];
|
|
1736
2732
|
const add = /* @__PURE__ */ __name((value) => {
|
|
1737
2733
|
try {
|
|
1738
2734
|
const url = new URL(value || "", location.href);
|
|
1739
|
-
url.
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
out.push(url.toString());
|
|
2735
|
+
if (url.origin === origin && /^https?:$/.test(url.protocol) && !seen.has(url.href) && urls.length < 500) {
|
|
2736
|
+
seen.add(url.href);
|
|
2737
|
+
urls.push(url.href);
|
|
1743
2738
|
return true;
|
|
1744
2739
|
}
|
|
1745
2740
|
} catch {
|
|
1746
2741
|
}
|
|
1747
2742
|
return false;
|
|
1748
2743
|
}, "add");
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2744
|
+
if (options.phase === "cancel") {
|
|
2745
|
+
const active = globalThis.__zuckerExtScan;
|
|
2746
|
+
if (active?.id === options.id) {
|
|
2747
|
+
active.cancelled = true;
|
|
2748
|
+
active.controller?.abort();
|
|
1754
2749
|
}
|
|
2750
|
+
return {
|
|
2751
|
+
urls: [],
|
|
2752
|
+
cancelled: true
|
|
2753
|
+
};
|
|
1755
2754
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2755
|
+
if (options.phase !== "sitemap") {
|
|
2756
|
+
add(location.href);
|
|
2757
|
+
for (const a of document.querySelectorAll("a[href]")) add(a.getAttribute("href"));
|
|
2758
|
+
}
|
|
2759
|
+
if (options.phase === "links") return {
|
|
2760
|
+
urls,
|
|
2761
|
+
sitemapCount: 0
|
|
2762
|
+
};
|
|
2763
|
+
const scan = {
|
|
2764
|
+
id: options.id,
|
|
2765
|
+
cancelled: false,
|
|
2766
|
+
controller: null
|
|
2767
|
+
};
|
|
2768
|
+
globalThis.__zuckerExtScan?.controller?.abort();
|
|
2769
|
+
globalThis.__zuckerExtScan = scan;
|
|
2770
|
+
const deadline = Date.now() + 6e3;
|
|
2771
|
+
let incomplete = false, sitemapCount = 0;
|
|
2772
|
+
const queue = [
|
|
2773
|
+
new URL("/sitemap.xml", origin).href
|
|
2774
|
+
], visited = /* @__PURE__ */ new Set();
|
|
2775
|
+
const sameOrigin = /* @__PURE__ */ __name((value) => {
|
|
2776
|
+
try {
|
|
2777
|
+
const url = new URL(value, origin);
|
|
2778
|
+
return url.origin === origin && /^https?:$/.test(url.protocol) ? url.href : null;
|
|
2779
|
+
} catch {
|
|
2780
|
+
return null;
|
|
2781
|
+
}
|
|
2782
|
+
}, "sameOrigin");
|
|
2783
|
+
async function read(url) {
|
|
2784
|
+
if (scan.cancelled || Date.now() >= deadline) {
|
|
2785
|
+
incomplete = true;
|
|
2786
|
+
return null;
|
|
1764
2787
|
}
|
|
1765
|
-
|
|
2788
|
+
const controller = new AbortController();
|
|
2789
|
+
scan.controller = controller;
|
|
2790
|
+
const timer = setTimeout(() => controller.abort(), Math.min(1500, deadline - Date.now()));
|
|
1766
2791
|
try {
|
|
1767
|
-
const response = await fetch(
|
|
1768
|
-
credentials: "same-origin"
|
|
2792
|
+
const response = await fetch(url, {
|
|
2793
|
+
credentials: "same-origin",
|
|
2794
|
+
signal: controller.signal
|
|
1769
2795
|
});
|
|
1770
|
-
if (!response.ok)
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
} else if (add(value)) {
|
|
1788
|
-
sitemapCount += 1;
|
|
2796
|
+
if (!response.ok) return null;
|
|
2797
|
+
if (Number(response.headers.get("content-length")) > 2 * 1024 * 1024) throw new Error("Sitemap too large");
|
|
2798
|
+
let stream = response.body;
|
|
2799
|
+
const reader = stream.getReader();
|
|
2800
|
+
const first = await reader.read();
|
|
2801
|
+
const gzip = first.value?.[0] === 31 && first.value?.[1] === 139;
|
|
2802
|
+
stream = new ReadableStream({
|
|
2803
|
+
start(ctrl) {
|
|
2804
|
+
if (first.value) ctrl.enqueue(first.value);
|
|
2805
|
+
if (first.done) ctrl.close();
|
|
2806
|
+
},
|
|
2807
|
+
async pull(ctrl) {
|
|
2808
|
+
const value = await reader.read();
|
|
2809
|
+
value.done ? ctrl.close() : ctrl.enqueue(value.value);
|
|
2810
|
+
},
|
|
2811
|
+
cancel(reason) {
|
|
2812
|
+
return reader.cancel(reason);
|
|
1789
2813
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
2814
|
+
});
|
|
2815
|
+
if (gzip) stream = stream.pipeThrough(new DecompressionStream("gzip"));
|
|
2816
|
+
const decoded = stream.getReader(), chunks = [];
|
|
2817
|
+
let size = 0;
|
|
2818
|
+
try {
|
|
2819
|
+
while (true) {
|
|
2820
|
+
const next = await decoded.read();
|
|
2821
|
+
if (next.done) break;
|
|
2822
|
+
size += next.value.length;
|
|
2823
|
+
if (size > 2 * 1024 * 1024) throw new Error("Sitemap too large");
|
|
2824
|
+
chunks.push(next.value);
|
|
1792
2825
|
}
|
|
2826
|
+
} finally {
|
|
2827
|
+
await decoded.cancel().catch(() => {
|
|
2828
|
+
});
|
|
1793
2829
|
}
|
|
2830
|
+
return await new Blob(chunks).text();
|
|
1794
2831
|
} catch {
|
|
2832
|
+
incomplete = true;
|
|
2833
|
+
return null;
|
|
2834
|
+
} finally {
|
|
2835
|
+
clearTimeout(timer);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
__name(read, "read");
|
|
2839
|
+
const robots = await read(new URL("/robots.txt", origin).href);
|
|
2840
|
+
for (const match of (robots || "").matchAll(/^sitemap:\s*(\S+)/gim)) {
|
|
2841
|
+
const url = sameOrigin(match[1]);
|
|
2842
|
+
if (url) queue.push(url);
|
|
2843
|
+
}
|
|
2844
|
+
while (queue.length && visited.size < 8 && urls.length < 500 && !scan.cancelled && Date.now() < deadline) {
|
|
2845
|
+
const url = queue.shift();
|
|
2846
|
+
if (visited.has(url)) continue;
|
|
2847
|
+
visited.add(url);
|
|
2848
|
+
const content = await read(url);
|
|
2849
|
+
if (!content) continue;
|
|
2850
|
+
const xml = new DOMParser().parseFromString(content, "application/xml");
|
|
2851
|
+
if (xml.querySelector("parsererror")) {
|
|
2852
|
+
incomplete = true;
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
const index = xml.documentElement.localName === "sitemapindex";
|
|
2856
|
+
for (const loc of xml.querySelectorAll("loc")) {
|
|
2857
|
+
const value = sameOrigin(loc.textContent?.trim());
|
|
2858
|
+
if (!value) continue;
|
|
2859
|
+
if (index) {
|
|
2860
|
+
if (!visited.has(value)) queue.push(value);
|
|
2861
|
+
} else if (add(value)) sitemapCount++;
|
|
1795
2862
|
}
|
|
1796
2863
|
}
|
|
2864
|
+
if (queue.length) incomplete = true;
|
|
2865
|
+
if (globalThis.__zuckerExtScan === scan) delete globalThis.__zuckerExtScan;
|
|
1797
2866
|
return {
|
|
1798
|
-
urls
|
|
1799
|
-
sitemapCount
|
|
2867
|
+
urls,
|
|
2868
|
+
sitemapCount,
|
|
2869
|
+
incomplete,
|
|
2870
|
+
cancelled: scan.cancelled
|
|
1800
2871
|
};
|
|
1801
2872
|
}
|
|
1802
2873
|
__name(collectSameOriginLinks, "collectSameOriginLinks");
|
|
2874
|
+
|
|
2875
|
+
// src/capture-budget.js
|
|
2876
|
+
var CAPTURE_LIMITS = {
|
|
2877
|
+
maxEdgeDevicePx: 16384,
|
|
2878
|
+
maxDevicePixels: 64 * 1024 * 1024
|
|
2879
|
+
};
|
|
2880
|
+
function captureScale(dpr) {
|
|
2881
|
+
return Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
|
|
2882
|
+
}
|
|
2883
|
+
__name(captureScale, "captureScale");
|
|
2884
|
+
function captureFits(width, height, dpr) {
|
|
2885
|
+
const scale = captureScale(dpr);
|
|
2886
|
+
return [
|
|
2887
|
+
width,
|
|
2888
|
+
height
|
|
2889
|
+
].every((n) => Number.isFinite(n) && n > 0) && width * scale <= CAPTURE_LIMITS.maxEdgeDevicePx && height * scale <= CAPTURE_LIMITS.maxEdgeDevicePx && width * height * scale ** 2 <= CAPTURE_LIMITS.maxDevicePixels;
|
|
2890
|
+
}
|
|
2891
|
+
__name(captureFits, "captureFits");
|
|
2892
|
+
function assertCaptureFits(width, height, dpr, message) {
|
|
2893
|
+
if (!captureFits(width, height, dpr)) throw new Error(message);
|
|
2894
|
+
}
|
|
2895
|
+
__name(assertCaptureFits, "assertCaptureFits");
|
|
2896
|
+
function captureBudget(dpr) {
|
|
2897
|
+
const scale = captureScale(dpr);
|
|
2898
|
+
return {
|
|
2899
|
+
maxEdge: Math.floor(CAPTURE_LIMITS.maxEdgeDevicePx / scale),
|
|
2900
|
+
maxArea: Math.floor(CAPTURE_LIMITS.maxDevicePixels / scale ** 2)
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
__name(captureBudget, "captureBudget");
|
|
2904
|
+
async function readDevicePixelRatio(ctx) {
|
|
2905
|
+
const result = await ctx.send("Runtime.evaluate", {
|
|
2906
|
+
expression: "devicePixelRatio || 1",
|
|
2907
|
+
returnByValue: true
|
|
2908
|
+
});
|
|
2909
|
+
return captureScale(result?.result?.value);
|
|
2910
|
+
}
|
|
2911
|
+
__name(readDevicePixelRatio, "readDevicePixelRatio");
|
|
1803
2912
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1804
2913
|
0 && (module.exports = {
|
|
2914
|
+
CAPTURE_LIMITS,
|
|
2915
|
+
assertCaptureFits,
|
|
1805
2916
|
buildBatchFileName,
|
|
1806
2917
|
buildQuickFileName,
|
|
2918
|
+
captureBudget,
|
|
2919
|
+
captureFits,
|
|
2920
|
+
collectSameOriginLinks,
|
|
2921
|
+
createDownloader,
|
|
1807
2922
|
createLicensing,
|
|
1808
2923
|
fileStamp,
|
|
1809
2924
|
folderNameFor,
|
|
2925
|
+
hidePageCaptureDecorations,
|
|
1810
2926
|
hostOf,
|
|
1811
2927
|
initExtension,
|
|
1812
2928
|
initPopup,
|
|
1813
2929
|
normalizeUrls,
|
|
2930
|
+
readDevicePixelRatio,
|
|
1814
2931
|
sanitize,
|
|
2932
|
+
scrollCapture,
|
|
1815
2933
|
sessionStamp,
|
|
2934
|
+
showPageCaptureDecorations,
|
|
1816
2935
|
todayString,
|
|
1817
2936
|
urlBaseName
|
|
1818
2937
|
});
|