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