@zucker-framework/ext-kit 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/assets/offscreen.js +20 -4
- package/dist/ext-kit-popup.js +438 -154
- package/dist/ext-kit.js +1605 -663
- package/dist/index.d.mts +132 -31
- package/dist/index.d.ts +132 -31
- package/dist/index.js +2075 -956
- package/dist/index.mjs +2069 -956
- package/dist/scroll-capture.js +309 -0
- package/package.json +4 -2
package/dist/ext-kit.js
CHANGED
|
@@ -1,24 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/util.js
|
|
10
|
+
function cancelled() {
|
|
11
|
+
return new Error("Cancelled");
|
|
6
12
|
}
|
|
7
|
-
function
|
|
8
|
-
|
|
13
|
+
function throwIfAborted(signal) {
|
|
14
|
+
if (signal?.aborted) throw cancelled();
|
|
9
15
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
16
|
+
function withAbort(promise, signal) {
|
|
17
|
+
if (!signal) return promise;
|
|
18
|
+
if (signal.aborted) {
|
|
19
|
+
promise.catch(() => {
|
|
20
|
+
});
|
|
21
|
+
return Promise.reject(cancelled());
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const abort = () => reject(cancelled());
|
|
25
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
26
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
27
|
+
});
|
|
12
28
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
29
|
+
function delay(ms, signal) {
|
|
30
|
+
throwIfAborted(signal);
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const finish = () => {
|
|
33
|
+
signal?.removeEventListener("abort", abort);
|
|
34
|
+
resolve();
|
|
35
|
+
};
|
|
36
|
+
const timer = setTimeout(finish, ms);
|
|
37
|
+
const abort = () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
reject(cancelled());
|
|
40
|
+
};
|
|
41
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
42
|
+
});
|
|
15
43
|
}
|
|
16
|
-
function
|
|
17
|
-
return
|
|
44
|
+
function normalizeError(error) {
|
|
45
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
46
|
}
|
|
19
|
-
function
|
|
20
|
-
return new Promise(
|
|
47
|
+
function storageCall(area, method, value) {
|
|
48
|
+
return new Promise(
|
|
49
|
+
(resolve, reject) => chrome.storage[area][method](value, (result) => {
|
|
50
|
+
const error = chrome.runtime.lastError;
|
|
51
|
+
error ? reject(new Error(error.message)) : resolve(result);
|
|
52
|
+
})
|
|
53
|
+
);
|
|
21
54
|
}
|
|
55
|
+
var storageGet = (keys) => storageCall("local", "get", keys);
|
|
56
|
+
var storageSet = (value) => storageCall("local", "set", value);
|
|
57
|
+
var storageRemove = (keys) => storageCall("local", "remove", keys);
|
|
22
58
|
|
|
23
59
|
// src/naming.ts
|
|
24
60
|
function sanitize(value) {
|
|
@@ -34,7 +70,7 @@
|
|
|
34
70
|
function normalizeUrls(urls) {
|
|
35
71
|
const out = [];
|
|
36
72
|
const seen = /* @__PURE__ */ new Set();
|
|
37
|
-
for (const raw of urls
|
|
73
|
+
for (const raw of Array.isArray(urls) ? urls : []) {
|
|
38
74
|
const value = String(raw || "").trim();
|
|
39
75
|
if (!/^https?:\/\//i.test(value)) {
|
|
40
76
|
continue;
|
|
@@ -42,7 +78,6 @@
|
|
|
42
78
|
let key = value;
|
|
43
79
|
try {
|
|
44
80
|
const parsed = new URL(value);
|
|
45
|
-
parsed.hash = "";
|
|
46
81
|
key = parsed.toString();
|
|
47
82
|
} catch {
|
|
48
83
|
continue;
|
|
@@ -112,226 +147,779 @@
|
|
|
112
147
|
return `${brand}/${quickFolder}/${base}-${fileStamp()}.${extension}`;
|
|
113
148
|
}
|
|
114
149
|
|
|
115
|
-
// src/licensing.
|
|
116
|
-
var CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
117
|
-
function unwrap(payload) {
|
|
118
|
-
const obj = payload;
|
|
119
|
-
return obj && typeof obj === "object" && "result" in obj ? obj.result : obj || {};
|
|
120
|
-
}
|
|
150
|
+
// src/licensing.js
|
|
121
151
|
function createLicensing(product, brand, config) {
|
|
152
|
+
for (const key of ["cacheTtlMs", "offlineGraceMs", "requestTimeoutMs", "retryDelayMs"]) {
|
|
153
|
+
if (!Number.isFinite(config[key]) || config[key] < 0 || key === "requestTimeoutMs" && !config[key])
|
|
154
|
+
throw new RangeError(`Invalid licensing ${key}`);
|
|
155
|
+
}
|
|
122
156
|
const TOKEN_KEY = `${product}_license_token`;
|
|
123
157
|
const ENTITLEMENT_KEY = `${product}_entitlement`;
|
|
124
158
|
const DEVICE_KEY = `${product}_device_id`;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
let refresh = null;
|
|
160
|
+
let nextRefreshAt = 0;
|
|
161
|
+
let generation = 0;
|
|
162
|
+
let mutation = false;
|
|
163
|
+
let lastStatus = null;
|
|
164
|
+
const unavailable = () => ({
|
|
165
|
+
state: "unavailable",
|
|
166
|
+
message: "Could not verify your saved Pro license. Check your connection and use Recheck Pro in License. Your activation is still saved."
|
|
167
|
+
});
|
|
168
|
+
const proStatus = (entitlement) => ({ state: "pro", entitlement });
|
|
169
|
+
function fallbackStatus(cached) {
|
|
170
|
+
if (lastStatus?.state === "invalid") return lastStatus;
|
|
171
|
+
const cachedEntitlement = usableCache(cached);
|
|
172
|
+
return cachedEntitlement ? proStatus(cachedEntitlement) : unavailable();
|
|
173
|
+
}
|
|
174
|
+
function normalize(value) {
|
|
175
|
+
if (!value || value.invalid === true) return null;
|
|
176
|
+
const normalized = config.normalizeEntitlement(value);
|
|
177
|
+
if (!normalized || typeof normalized.expiresAt !== "string") return null;
|
|
178
|
+
const expires = Date.parse(normalized.expiresAt);
|
|
179
|
+
return Number.isFinite(expires) && expires > Date.now() ? normalized : null;
|
|
180
|
+
}
|
|
181
|
+
function usableCache(cached) {
|
|
182
|
+
const age = Date.now() - Number(cached?.checkedAt);
|
|
183
|
+
return Number.isFinite(age) && age >= 0 && age < config.offlineGraceMs ? normalize(cached) : null;
|
|
184
|
+
}
|
|
185
|
+
async function request(operation, input = {}) {
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const timer = setTimeout(() => controller.abort(), config.requestTimeoutMs);
|
|
188
|
+
try {
|
|
189
|
+
return await withAbort(Promise.resolve().then(() => config.request(operation, input, controller.signal)), controller.signal);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"License service unreachable. Check your connection and retry."
|
|
193
|
+
);
|
|
194
|
+
} finally {
|
|
195
|
+
clearTimeout(timer);
|
|
129
196
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
197
|
+
}
|
|
198
|
+
async function getDeviceId() {
|
|
199
|
+
const stored = await storageGet(DEVICE_KEY);
|
|
200
|
+
if (stored[DEVICE_KEY]) return stored[DEVICE_KEY];
|
|
201
|
+
const id = crypto.randomUUID();
|
|
133
202
|
await storageSet({ [DEVICE_KEY]: id });
|
|
134
203
|
return id;
|
|
135
204
|
}
|
|
136
205
|
async function activate(code) {
|
|
137
|
-
|
|
138
|
-
|
|
206
|
+
if (mutation) throw new Error("A license change is already in progress");
|
|
207
|
+
mutation = true;
|
|
208
|
+
generation += 1;
|
|
139
209
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
210
|
+
const deviceId = await getDeviceId();
|
|
211
|
+
const data = await request("activate", { code, deviceId, brand });
|
|
212
|
+
if (!data.accepted)
|
|
213
|
+
throw new Error(data.denied ? "Invalid or expired code, or its device limit is reached." : "Activation failed. Please retry later.");
|
|
214
|
+
const entitlement = normalize(data?.entitlement);
|
|
215
|
+
if (!entitlement || typeof data?.token !== "string" || !data.token)
|
|
216
|
+
throw new Error(
|
|
217
|
+
"License service returned invalid activation data. Please retry."
|
|
218
|
+
);
|
|
219
|
+
await storageSet({
|
|
220
|
+
[TOKEN_KEY]: data.token,
|
|
221
|
+
[ENTITLEMENT_KEY]: { ...entitlement, checkedAt: Date.now() }
|
|
149
222
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (response.status === 403) {
|
|
156
|
-
throw new Error("Invalid code, or its device limit is reached.");
|
|
157
|
-
}
|
|
158
|
-
throw new Error("Activation failed. Please retry later.");
|
|
223
|
+
nextRefreshAt = 0;
|
|
224
|
+
lastStatus = proStatus(entitlement);
|
|
225
|
+
return entitlement;
|
|
226
|
+
} finally {
|
|
227
|
+
mutation = false;
|
|
159
228
|
}
|
|
160
|
-
const data = unwrap(payload);
|
|
161
|
-
await storageSet({
|
|
162
|
-
[TOKEN_KEY]: data.token || "",
|
|
163
|
-
[ENTITLEMENT_KEY]: { ...data.entitlement || {}, checkedAt: Date.now() }
|
|
164
|
-
});
|
|
165
|
-
return data.entitlement;
|
|
166
229
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return null;
|
|
230
|
+
async function refreshEntitlement(token, cached, version) {
|
|
231
|
+
try {
|
|
232
|
+
const data = await request("me", { token });
|
|
233
|
+
if (version !== generation) return null;
|
|
234
|
+
if (data.denied === true) {
|
|
235
|
+
const denied = {
|
|
236
|
+
state: "invalid",
|
|
237
|
+
message: "Your saved Pro license could not be validated. Use Recheck Pro or enter your original activation code in License to restore it."
|
|
238
|
+
};
|
|
239
|
+
lastStatus = denied;
|
|
240
|
+
try {
|
|
241
|
+
await storageRemove(ENTITLEMENT_KEY);
|
|
242
|
+
} catch {
|
|
243
|
+
await storageSet({
|
|
244
|
+
[ENTITLEMENT_KEY]: { invalid: true, active: false, checkedAt: Date.now() }
|
|
245
|
+
}).catch(() => {
|
|
246
|
+
console.warn(
|
|
247
|
+
`${brand}: rejected license cache could not be invalidated`
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return denied;
|
|
252
|
+
}
|
|
253
|
+
const entitlement = data.accepted ? normalize(data.entitlement) : null;
|
|
254
|
+
if (entitlement) {
|
|
255
|
+
await storageSet({
|
|
256
|
+
[ENTITLEMENT_KEY]: { ...entitlement, checkedAt: Date.now() }
|
|
257
|
+
});
|
|
258
|
+
return proStatus(entitlement);
|
|
259
|
+
}
|
|
260
|
+
return fallbackStatus(cached);
|
|
261
|
+
} catch {
|
|
262
|
+
if (version !== generation) return null;
|
|
263
|
+
return fallbackStatus(cached);
|
|
171
264
|
}
|
|
172
|
-
const expired = v.expiresAt && new Date(v.expiresAt).getTime() <= Date.now();
|
|
173
|
-
return expired ? null : { plan: v.plan || "pro", expiresAt: v.expiresAt || "" };
|
|
174
265
|
}
|
|
175
|
-
async function
|
|
266
|
+
async function getStatus({ cachedOnly = false, force = false } = {}) {
|
|
176
267
|
const data = await storageGet([TOKEN_KEY, ENTITLEMENT_KEY]);
|
|
177
|
-
const token = data[TOKEN_KEY];
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
268
|
+
const token = data[TOKEN_KEY], cached = data[ENTITLEMENT_KEY];
|
|
269
|
+
if (!token) return { state: "free" };
|
|
270
|
+
const valid = lastStatus?.state === "invalid" ? null : usableCache(cached);
|
|
271
|
+
if (!force && valid && Date.now() - cached.checkedAt < config.cacheTtlMs)
|
|
272
|
+
return proStatus(valid);
|
|
273
|
+
if (mutation) return { state: "checking", refreshing: true };
|
|
274
|
+
if (!refresh && (force || Date.now() >= nextRefreshAt)) {
|
|
275
|
+
const version = generation;
|
|
276
|
+
nextRefreshAt = Date.now() + config.retryDelayMs;
|
|
277
|
+
refresh = refreshEntitlement(token, cached, version).then((status2) => {
|
|
278
|
+
if (version === generation && status2) lastStatus = status2;
|
|
279
|
+
return status2;
|
|
280
|
+
}).finally(() => {
|
|
281
|
+
refresh = null;
|
|
189
282
|
});
|
|
190
|
-
} catch {
|
|
191
|
-
return cached ? normalize(cached) : null;
|
|
192
283
|
}
|
|
193
|
-
if (!
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
return cached ? normalize(cached) : null;
|
|
284
|
+
if (refresh && !cachedOnly) {
|
|
285
|
+
const status2 = await refresh;
|
|
286
|
+
return status2 || getStatus();
|
|
199
287
|
}
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
[ENTITLEMENT_KEY]: { ...entitlement || {}, checkedAt: Date.now() }
|
|
204
|
-
});
|
|
205
|
-
return normalize(entitlement);
|
|
288
|
+
const remembered = lastStatus?.state === "pro" ? null : lastStatus;
|
|
289
|
+
const status = valid ? proStatus(valid) : refresh ? { state: "checking" } : remembered || unavailable();
|
|
290
|
+
return { ...status, refreshing: Boolean(refresh) };
|
|
206
291
|
}
|
|
207
|
-
async function
|
|
208
|
-
return
|
|
292
|
+
async function getEntitlement(options) {
|
|
293
|
+
return (await getStatus(options)).entitlement || null;
|
|
209
294
|
}
|
|
210
|
-
async function
|
|
211
|
-
|
|
295
|
+
async function deactivate() {
|
|
296
|
+
if (mutation) throw new Error("A license change is already in progress");
|
|
297
|
+
mutation = true;
|
|
298
|
+
generation += 1;
|
|
299
|
+
try {
|
|
300
|
+
const data = await storageGet(TOKEN_KEY);
|
|
301
|
+
if (data[TOKEN_KEY]) {
|
|
302
|
+
const result = await request("deactivate", { token: data[TOKEN_KEY] });
|
|
303
|
+
if (!result.accepted || result.deactivated !== true)
|
|
304
|
+
throw new Error(
|
|
305
|
+
"Could not release this device. Your license is still saved; please retry."
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
await storageRemove([TOKEN_KEY, ENTITLEMENT_KEY]);
|
|
309
|
+
nextRefreshAt = 0;
|
|
310
|
+
lastStatus = null;
|
|
311
|
+
} finally {
|
|
312
|
+
mutation = false;
|
|
313
|
+
}
|
|
212
314
|
}
|
|
213
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
activate,
|
|
317
|
+
getEntitlement,
|
|
318
|
+
getStatus,
|
|
319
|
+
deactivate,
|
|
320
|
+
purchaseUrl: config.purchaseUrl
|
|
321
|
+
};
|
|
214
322
|
}
|
|
215
323
|
|
|
216
|
-
// src/download.
|
|
217
|
-
function createDownloader(justification) {
|
|
218
|
-
const
|
|
324
|
+
// src/download.js
|
|
325
|
+
function createDownloader(justification, options = {}) {
|
|
326
|
+
const deadlineMs = options.deadlineMs ?? 12e4;
|
|
327
|
+
const blobLeaseMs = options.blobLeaseMs ?? 18e4;
|
|
328
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
329
|
+
if (![deadlineMs, blobLeaseMs, pollMs].every((n) => Number.isFinite(n) && n > 0) || blobLeaseMs <= deadlineMs)
|
|
330
|
+
throw new RangeError("Invalid download deadline, polling or blob lease");
|
|
331
|
+
const names = /* @__PURE__ */ new Map();
|
|
332
|
+
const waiters = /* @__PURE__ */ new Map();
|
|
219
333
|
let offscreenSetup = null;
|
|
220
334
|
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
335
|
+
const filename = names.get(item.url) || names.get(item.finalUrl);
|
|
336
|
+
if (filename) suggest({ filename, conflictAction: "uniquify" });
|
|
337
|
+
});
|
|
338
|
+
chrome.downloads.onChanged.addListener((delta) => {
|
|
339
|
+
if (delta.state?.current === "complete" || delta.state?.current === "interrupted")
|
|
340
|
+
waiters.get(delta.id)?.();
|
|
227
341
|
});
|
|
228
342
|
async function ensureOffscreen() {
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}).
|
|
238
|
-
if (!String(error?.message || "").includes("Only a single")) {
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
241
|
-
}).finally(() => {
|
|
343
|
+
if (!offscreenSetup)
|
|
344
|
+
offscreenSetup = (async () => {
|
|
345
|
+
if (!await chrome.offscreen.hasDocument())
|
|
346
|
+
await chrome.offscreen.createDocument({
|
|
347
|
+
url: options.offscreenUrl || "offscreen.html",
|
|
348
|
+
reasons: [chrome.offscreen.Reason.BLOBS],
|
|
349
|
+
justification
|
|
350
|
+
});
|
|
351
|
+
})().finally(() => {
|
|
242
352
|
offscreenSetup = null;
|
|
243
353
|
});
|
|
244
|
-
}
|
|
245
354
|
return offscreenSetup;
|
|
246
355
|
}
|
|
247
|
-
async function
|
|
356
|
+
async function cleanup(url) {
|
|
357
|
+
names.delete(url);
|
|
358
|
+
if (url)
|
|
359
|
+
await chrome.runtime.sendMessage({ target: "offscreen", type: "REVOKE_BLOB_URL", url }).catch(() => {
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async function waitForDownload(id, signal) {
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
let finished = false;
|
|
365
|
+
let deadlineError;
|
|
366
|
+
const finish = (error, item) => {
|
|
367
|
+
if (finished) return;
|
|
368
|
+
finished = true;
|
|
369
|
+
clearInterval(poll);
|
|
370
|
+
clearTimeout(timeout);
|
|
371
|
+
signal?.removeEventListener("abort", abort);
|
|
372
|
+
waiters.delete(id);
|
|
373
|
+
error ? reject(error) : resolve(item);
|
|
374
|
+
};
|
|
375
|
+
const inspect = async () => {
|
|
376
|
+
try {
|
|
377
|
+
const [item] = await chrome.downloads.search({ id });
|
|
378
|
+
if (!item) return finish(new Error("Download no longer exists"));
|
|
379
|
+
if (item.state === "complete") finish(null, item);
|
|
380
|
+
else if (item.state === "interrupted")
|
|
381
|
+
finish(
|
|
382
|
+
deadlineError || new Error(
|
|
383
|
+
item.error === "USER_CANCELED" ? "Cancelled" : `Download interrupted: ${item.error || "unknown error"}`
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
finish(error);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const abort = async () => {
|
|
391
|
+
await chrome.downloads.cancel(id).catch(() => {
|
|
392
|
+
});
|
|
393
|
+
const [item] = await chrome.downloads.search({ id }).catch(() => []);
|
|
394
|
+
item?.state === "complete" ? finish(null, item) : finish(cancelled());
|
|
395
|
+
};
|
|
396
|
+
const poll = setInterval(inspect, pollMs);
|
|
397
|
+
const timeout = setTimeout(async () => {
|
|
398
|
+
deadlineError = new Error("Download completion timed out");
|
|
399
|
+
await chrome.downloads.cancel(id).catch(() => {
|
|
400
|
+
});
|
|
401
|
+
finish(deadlineError);
|
|
402
|
+
}, deadlineMs);
|
|
403
|
+
waiters.set(id, inspect);
|
|
404
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
405
|
+
if (signal?.aborted) void abort();
|
|
406
|
+
else void inspect();
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async function recover(item, signal) {
|
|
410
|
+
const matches = item.downloadId != null ? await chrome.downloads.search({ id: item.downloadId }) : await chrome.downloads.search({
|
|
411
|
+
url: `^${item.downloadUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`
|
|
412
|
+
});
|
|
413
|
+
const download = matches.find(
|
|
414
|
+
(d) => d.byExtensionId === chrome.runtime.id && (!item.downloadUrl || d.url === item.downloadUrl)
|
|
415
|
+
);
|
|
416
|
+
if (!download) {
|
|
417
|
+
await cleanup(item.downloadUrl);
|
|
418
|
+
throw new Error(
|
|
419
|
+
"Interrupted download could not be confirmed. Retry this page."
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
return await waitForDownload(download.id, signal);
|
|
424
|
+
} finally {
|
|
425
|
+
await cleanup(item.downloadUrl);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function downloadViaBlob(base64, mime, filename, { signal, onPrepared, onCreated } = {}) {
|
|
429
|
+
throwIfAborted(signal);
|
|
248
430
|
await ensureOffscreen();
|
|
431
|
+
throwIfAborted(signal);
|
|
249
432
|
const created = await chrome.runtime.sendMessage({
|
|
250
433
|
target: "offscreen",
|
|
251
434
|
type: "CREATE_BLOB_URL",
|
|
252
435
|
base64,
|
|
253
|
-
mime
|
|
436
|
+
mime,
|
|
437
|
+
leaseMs: blobLeaseMs
|
|
254
438
|
});
|
|
255
|
-
if (!created?.ok || !created.url)
|
|
439
|
+
if (!created?.ok || !created.url)
|
|
256
440
|
throw new Error(created?.error || "Failed to prepare download");
|
|
257
|
-
}
|
|
258
441
|
const url = created.url;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
pendingDownloadNames.delete(url);
|
|
262
|
-
chrome.runtime.sendMessage({ target: "offscreen", type: "REVOKE_BLOB_URL", url }).catch(() => {
|
|
263
|
-
});
|
|
264
|
-
};
|
|
442
|
+
names.set(url, filename);
|
|
443
|
+
let downloadId;
|
|
265
444
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
);
|
|
445
|
+
await onPrepared?.(url);
|
|
446
|
+
throwIfAborted(signal);
|
|
447
|
+
downloadId = await chrome.downloads.download({
|
|
448
|
+
url,
|
|
449
|
+
filename,
|
|
450
|
+
conflictAction: "uniquify",
|
|
451
|
+
saveAs: false
|
|
274
452
|
});
|
|
275
|
-
|
|
453
|
+
await onCreated?.(downloadId);
|
|
454
|
+
await waitForDownload(downloadId, signal);
|
|
276
455
|
return downloadId;
|
|
277
456
|
} catch (error) {
|
|
278
|
-
|
|
457
|
+
if (downloadId != null)
|
|
458
|
+
await chrome.downloads.cancel(downloadId).catch(() => {
|
|
459
|
+
});
|
|
279
460
|
throw error;
|
|
461
|
+
} finally {
|
|
462
|
+
await cleanup(url);
|
|
280
463
|
}
|
|
281
464
|
}
|
|
282
|
-
return { downloadViaBlob };
|
|
465
|
+
return { downloadViaBlob, recover };
|
|
283
466
|
}
|
|
284
467
|
|
|
285
|
-
// src/
|
|
468
|
+
// src/scroll-capture.js
|
|
469
|
+
var scroll_capture_exports = {};
|
|
470
|
+
__export(scroll_capture_exports, {
|
|
471
|
+
prepare: () => prepare,
|
|
472
|
+
restore: () => restore
|
|
473
|
+
});
|
|
474
|
+
var sessions = /* @__PURE__ */ new WeakMap();
|
|
475
|
+
function pageTask(action, id, index, leaseMs) {
|
|
476
|
+
const key = "__zuckerScrollCapture";
|
|
477
|
+
let state = window[key];
|
|
478
|
+
if (action === "restore") {
|
|
479
|
+
if (state?.id === id) state.restore();
|
|
480
|
+
return {};
|
|
481
|
+
}
|
|
482
|
+
if (action === "init") {
|
|
483
|
+
if (state) throw new Error("Another scrolling-content capture is active");
|
|
484
|
+
const elements = [...document.querySelectorAll("*")];
|
|
485
|
+
if (elements.length > 3e4)
|
|
486
|
+
throw new Error(
|
|
487
|
+
"This page is too complex to expand its scrolling content safely."
|
|
488
|
+
);
|
|
489
|
+
const panels = elements.filter((el) => {
|
|
490
|
+
if (el === document.body || el === document.documentElement || el === document.scrollingElement || el.closest(
|
|
491
|
+
'nav,aside,[role="navigation"],[role="menu"],textarea,select'
|
|
492
|
+
))
|
|
493
|
+
return false;
|
|
494
|
+
const rect = el.getBoundingClientRect(), css = getComputedStyle(el);
|
|
495
|
+
if (rect.width < 120 || rect.height < 80 || el.scrollHeight <= el.clientHeight + 2 || !/^(auto|scroll|overlay)$/.test(css.overflowY) || css.visibility !== "visible")
|
|
496
|
+
return false;
|
|
497
|
+
for (let parent = el; parent; parent = parent.parentElement) {
|
|
498
|
+
const style = getComputedStyle(parent);
|
|
499
|
+
if (style.display === "none" || Number(style.opacity) === 0)
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
return true;
|
|
503
|
+
});
|
|
504
|
+
if (!panels.length) return { count: 0 };
|
|
505
|
+
if (panels.length > 24)
|
|
506
|
+
throw new Error(
|
|
507
|
+
"This page has too many independent scrolling areas. Use Visible window or export smaller sections."
|
|
508
|
+
);
|
|
509
|
+
const virtual = '.rc-virtual-list-holder,.ant-table-tbody-virtual-holder,[data-virtuoso-scroller],.ReactVirtualized__Grid,.ReactVirtualized__List,[data-virtualized="true"]';
|
|
510
|
+
if (panels.some(
|
|
511
|
+
(el) => el.matches(virtual) || [...el.querySelectorAll(virtual)].some(
|
|
512
|
+
(list) => list.getClientRects().length && list.checkVisibility({
|
|
513
|
+
checkOpacity: true,
|
|
514
|
+
checkVisibilityCSS: true
|
|
515
|
+
})
|
|
516
|
+
)
|
|
517
|
+
))
|
|
518
|
+
throw new Error(
|
|
519
|
+
"This page uses a virtual scrolling list. A complete full-page capture is not available; use the table's export or Visible window."
|
|
520
|
+
);
|
|
521
|
+
const changes = /* @__PURE__ */ new Map(), scrolls = /* @__PURE__ */ new Map();
|
|
522
|
+
state = {
|
|
523
|
+
id,
|
|
524
|
+
panels,
|
|
525
|
+
changes,
|
|
526
|
+
scrolls,
|
|
527
|
+
boxes: /* @__PURE__ */ new Map(),
|
|
528
|
+
watchdog: null,
|
|
529
|
+
leaseMs
|
|
530
|
+
};
|
|
531
|
+
state.set = (el, property, value) => {
|
|
532
|
+
if (!changes.has(el))
|
|
533
|
+
changes.set(el, {
|
|
534
|
+
hadStyle: el.hasAttribute("style"),
|
|
535
|
+
properties: /* @__PURE__ */ new Map()
|
|
536
|
+
});
|
|
537
|
+
const saved = changes.get(el).properties;
|
|
538
|
+
if (!saved.has(property))
|
|
539
|
+
saved.set(property, {
|
|
540
|
+
value: el.style.getPropertyValue(property),
|
|
541
|
+
priority: el.style.getPropertyPriority(property)
|
|
542
|
+
});
|
|
543
|
+
el.style.setProperty(property, value, "important");
|
|
544
|
+
};
|
|
545
|
+
state.restore = () => {
|
|
546
|
+
clearTimeout(state.watchdog);
|
|
547
|
+
const scrollingProperties = /* @__PURE__ */ new Set([
|
|
548
|
+
"scroll-behavior",
|
|
549
|
+
"scroll-snap-type",
|
|
550
|
+
"overflow-anchor"
|
|
551
|
+
]);
|
|
552
|
+
const restoreProperty = (el, property, old) => old.value ? el.style.setProperty(property, old.value, old.priority) : el.style.removeProperty(property);
|
|
553
|
+
for (const [el, saved] of [...changes].reverse())
|
|
554
|
+
for (const [property, old] of saved.properties)
|
|
555
|
+
if (!scrollingProperties.has(property))
|
|
556
|
+
restoreProperty(el, property, old);
|
|
557
|
+
for (const [el, saved] of scrolls) {
|
|
558
|
+
el.scrollLeft = saved.x;
|
|
559
|
+
el.scrollTop = saved.y;
|
|
560
|
+
}
|
|
561
|
+
for (const [el, saved] of changes) {
|
|
562
|
+
for (const [property, old] of saved.properties)
|
|
563
|
+
if (scrollingProperties.has(property))
|
|
564
|
+
restoreProperty(el, property, old);
|
|
565
|
+
if (!saved.hadStyle && !el.style.length) {
|
|
566
|
+
el.getAttribute("style");
|
|
567
|
+
el.removeAttribute("style");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (window[key]?.id === id) delete window[key];
|
|
571
|
+
};
|
|
572
|
+
window[key] = state;
|
|
573
|
+
state.watchdog = setTimeout(state.restore, state.leaseMs);
|
|
574
|
+
for (const el of elements) {
|
|
575
|
+
if (!el.scrollTop && !el.scrollLeft) continue;
|
|
576
|
+
scrolls.set(el, { x: el.scrollLeft, y: el.scrollTop });
|
|
577
|
+
state.set(el, "scroll-behavior", "auto");
|
|
578
|
+
state.set(el, "scroll-snap-type", "none");
|
|
579
|
+
state.set(el, "overflow-anchor", "none");
|
|
580
|
+
}
|
|
581
|
+
for (const panel of panels) {
|
|
582
|
+
const rect = panel.getBoundingClientRect();
|
|
583
|
+
state.boxes.set(panel, { width: rect.width, x: rect.x });
|
|
584
|
+
scrolls.set(panel, { x: panel.scrollLeft, y: panel.scrollTop });
|
|
585
|
+
state.set(panel, "scroll-behavior", "auto");
|
|
586
|
+
state.set(panel, "scroll-snap-type", "none");
|
|
587
|
+
state.set(panel, "overflow-anchor", "none");
|
|
588
|
+
panel.scrollTop = 0;
|
|
589
|
+
}
|
|
590
|
+
return { count: panels.length };
|
|
591
|
+
}
|
|
592
|
+
if (!state || state.id !== id)
|
|
593
|
+
throw new Error(
|
|
594
|
+
"The page changed while preparing its scrolling content."
|
|
595
|
+
);
|
|
596
|
+
clearTimeout(state.watchdog);
|
|
597
|
+
state.watchdog = setTimeout(state.restore, state.leaseMs);
|
|
598
|
+
if (action === "scroll" || action === "position") {
|
|
599
|
+
const panel = state.panels[index];
|
|
600
|
+
if (!panel.isConnected)
|
|
601
|
+
throw new Error(
|
|
602
|
+
"A scrolling area changed during capture. Please retry."
|
|
603
|
+
);
|
|
604
|
+
if (action === "scroll")
|
|
605
|
+
panel.scrollTop += Math.max(80, panel.clientHeight * 0.8);
|
|
606
|
+
return {
|
|
607
|
+
bottom: panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 1,
|
|
608
|
+
height: panel.scrollHeight
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (action === "expand") {
|
|
612
|
+
for (const panel of [...state.panels].reverse()) {
|
|
613
|
+
if (!panel.isConnected)
|
|
614
|
+
throw new Error(
|
|
615
|
+
"A scrolling area changed during capture. Please retry."
|
|
616
|
+
);
|
|
617
|
+
let el = panel;
|
|
618
|
+
while (el) {
|
|
619
|
+
const rect = el.getBoundingClientRect(), css = getComputedStyle(el);
|
|
620
|
+
if (!state.scrolls.has(el))
|
|
621
|
+
state.scrolls.set(el, { x: el.scrollLeft, y: el.scrollTop });
|
|
622
|
+
if (!state.boxes.has(el))
|
|
623
|
+
state.boxes.set(el, { width: rect.width, x: rect.x });
|
|
624
|
+
state.set(el, "scroll-behavior", "auto");
|
|
625
|
+
state.set(el, "overflow-anchor", "none");
|
|
626
|
+
state.set(el, "transition", "none");
|
|
627
|
+
state.set(el, "box-sizing", "border-box");
|
|
628
|
+
state.set(el, "width", `${state.boxes.get(el).width}px`);
|
|
629
|
+
state.set(el, "max-height", "none");
|
|
630
|
+
state.set(el, "flex-shrink", "0");
|
|
631
|
+
if (css.position === "fixed") {
|
|
632
|
+
state.set(el, "position", "absolute");
|
|
633
|
+
state.set(el, "top", `${rect.top + scrollY}px`);
|
|
634
|
+
state.set(el, "left", `${rect.left + scrollX}px`);
|
|
635
|
+
state.set(el, "right", "auto");
|
|
636
|
+
}
|
|
637
|
+
if (css.position === "fixed" || css.position === "absolute")
|
|
638
|
+
state.set(el, "bottom", "auto");
|
|
639
|
+
if (state.panels.includes(el)) {
|
|
640
|
+
const height = el.scrollHeight + el.offsetHeight - el.clientHeight;
|
|
641
|
+
state.set(el, "height", `${height}px`);
|
|
642
|
+
state.set(el, "min-height", `${height}px`);
|
|
643
|
+
state.set(el, "scrollbar-gutter", "stable");
|
|
644
|
+
state.set(el, "overflow-y", "hidden");
|
|
645
|
+
el.scrollTop = 0;
|
|
646
|
+
} else {
|
|
647
|
+
state.set(el, "height", "auto");
|
|
648
|
+
state.set(
|
|
649
|
+
el,
|
|
650
|
+
"min-height",
|
|
651
|
+
`${Math.max(rect.height, el.clientHeight)}px`
|
|
652
|
+
);
|
|
653
|
+
if (/hidden|clip|auto|scroll/.test(css.overflowY)) {
|
|
654
|
+
state.set(el, "overflow-x", "clip");
|
|
655
|
+
state.set(el, "overflow-y", "visible");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (css.contain !== "none") state.set(el, "contain", "none");
|
|
659
|
+
el = el.parentElement;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { height: document.documentElement.scrollHeight };
|
|
663
|
+
}
|
|
664
|
+
if (action === "measure") {
|
|
665
|
+
for (const panel of state.panels) {
|
|
666
|
+
const rect = panel.getBoundingClientRect(), original = state.boxes.get(panel);
|
|
667
|
+
if (Math.abs(rect.width - original.width) > 1 || Math.abs(rect.x - original.x) > 1)
|
|
668
|
+
throw new Error(
|
|
669
|
+
"This scrolling layout cannot be expanded without changing its width. Use Visible window or the website's export."
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
height: Math.max(
|
|
674
|
+
document.documentElement.scrollHeight,
|
|
675
|
+
document.body?.scrollHeight || 0
|
|
676
|
+
),
|
|
677
|
+
panels: state.panels.map((el) => [el.scrollHeight, el.clientHeight]),
|
|
678
|
+
ready: document.fonts.status === "loaded" && [...document.images].every(
|
|
679
|
+
(image) => image.complete || !state.panels.some((panel) => panel.contains(image)) || !image.getClientRects().length || image.checkVisibility?.({
|
|
680
|
+
checkOpacity: true,
|
|
681
|
+
checkVisibilityCSS: true
|
|
682
|
+
}) === false
|
|
683
|
+
)
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return {};
|
|
687
|
+
}
|
|
688
|
+
async function command(ctx, id, action, index, leaseMs) {
|
|
689
|
+
const response = await ctx[action === "restore" ? "cleanup" : "send"](
|
|
690
|
+
"Runtime.evaluate",
|
|
691
|
+
{
|
|
692
|
+
expression: `(${pageTask.toString()})(${JSON.stringify(action)},${JSON.stringify(id)},${JSON.stringify(index)},${JSON.stringify(leaseMs)})`,
|
|
693
|
+
returnByValue: true
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
if (response?.exceptionDetails)
|
|
697
|
+
throw new Error(
|
|
698
|
+
response.exceptionDetails.exception?.description?.split("\n")[0] || "Could not prepare scrolling content"
|
|
699
|
+
);
|
|
700
|
+
return response?.result?.value || {};
|
|
701
|
+
}
|
|
702
|
+
async function prepare(ctx, waitForContent = async () => {
|
|
703
|
+
}) {
|
|
704
|
+
const id = crypto.randomUUID();
|
|
705
|
+
sessions.set(ctx, id);
|
|
706
|
+
const readinessMs = Math.min(
|
|
707
|
+
6e4,
|
|
708
|
+
Math.max(0, Number(ctx.options?.readyTimeoutMs) || 25e3)
|
|
709
|
+
);
|
|
710
|
+
const initial = await command(
|
|
711
|
+
ctx,
|
|
712
|
+
id,
|
|
713
|
+
"init",
|
|
714
|
+
null,
|
|
715
|
+
Math.max(6e4, readinessMs + 15e3)
|
|
716
|
+
);
|
|
717
|
+
if (!initial.count) {
|
|
718
|
+
sessions.delete(ctx);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
let deadline = Date.now() + readinessMs + 15e3;
|
|
722
|
+
for (let index = 0; index < initial.count; index++) {
|
|
723
|
+
let bottom = false;
|
|
724
|
+
for (let step = 0; step < 60 && Date.now() < deadline; step++) {
|
|
725
|
+
const position = await command(ctx, id, "scroll", index);
|
|
726
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
727
|
+
if (position.bottom) {
|
|
728
|
+
await waitForContent();
|
|
729
|
+
const loaded = await command(ctx, id, "position", index);
|
|
730
|
+
if (loaded.bottom && loaded.height === position.height) {
|
|
731
|
+
bottom = true;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (!bottom)
|
|
737
|
+
throw new Error(
|
|
738
|
+
"Scrolling content keeps growing or exceeds the capture limit. Use Visible window or a smaller section."
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
await waitForContent();
|
|
742
|
+
await command(ctx, id, "expand");
|
|
743
|
+
await waitForContent();
|
|
744
|
+
deadline = Date.now() + 15e3;
|
|
745
|
+
let previous = "", stable = 0;
|
|
746
|
+
while (Date.now() < deadline) {
|
|
747
|
+
await command(ctx, id, "expand");
|
|
748
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
749
|
+
const measured = await command(ctx, id, "measure");
|
|
750
|
+
const signature = JSON.stringify([measured.height, measured.panels]);
|
|
751
|
+
stable = measured.ready && signature === previous && measured.panels.every(([full, view]) => full <= view + 2) ? stable + 1 : 0;
|
|
752
|
+
if (stable >= 2) return;
|
|
753
|
+
previous = signature;
|
|
754
|
+
}
|
|
755
|
+
throw new Error(
|
|
756
|
+
"Scrolling content did not settle in time. Wait for it to finish loading and retry."
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
async function restore(ctx) {
|
|
760
|
+
const id = sessions.get(ctx);
|
|
761
|
+
if (!id) return;
|
|
762
|
+
sessions.delete(ctx);
|
|
763
|
+
await command(ctx, id, "restore");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/session.js
|
|
286
767
|
function debuggerAttach(target, version) {
|
|
287
|
-
return new Promise(
|
|
288
|
-
chrome.debugger.attach(target, version, () => {
|
|
768
|
+
return new Promise(
|
|
769
|
+
(resolve, reject) => chrome.debugger.attach(target, version, () => {
|
|
289
770
|
const error = chrome.runtime.lastError;
|
|
290
771
|
error ? reject(new Error(error.message)) : resolve();
|
|
291
|
-
})
|
|
292
|
-
|
|
772
|
+
})
|
|
773
|
+
);
|
|
293
774
|
}
|
|
294
775
|
function debuggerDetach(target) {
|
|
295
|
-
return new Promise(
|
|
296
|
-
chrome.debugger.detach(target, () => {
|
|
776
|
+
return new Promise(
|
|
777
|
+
(resolve, reject) => chrome.debugger.detach(target, () => {
|
|
297
778
|
const error = chrome.runtime.lastError;
|
|
298
779
|
error ? reject(new Error(error.message)) : resolve();
|
|
299
|
-
})
|
|
300
|
-
|
|
780
|
+
})
|
|
781
|
+
);
|
|
301
782
|
}
|
|
302
|
-
function sendCommand(target, method, params) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
783
|
+
function sendCommand(target, method, params, signal) {
|
|
784
|
+
throwIfAborted(signal);
|
|
785
|
+
return withAbort(
|
|
786
|
+
new Promise(
|
|
787
|
+
(resolve, reject) => chrome.debugger.sendCommand(target, method, params || {}, (result) => {
|
|
788
|
+
const error = chrome.runtime.lastError;
|
|
789
|
+
if (error) reject(new Error(error.message));
|
|
790
|
+
else if (result?.exceptionDetails)
|
|
791
|
+
reject(new Error("Page script could not be evaluated"));
|
|
792
|
+
else resolve(result || {});
|
|
793
|
+
})
|
|
794
|
+
),
|
|
795
|
+
signal
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
async function waitForLoad(tabId, timeoutMs, signal) {
|
|
799
|
+
const start = Date.now();
|
|
800
|
+
while (Date.now() - start < timeoutMs) {
|
|
801
|
+
throwIfAborted(signal);
|
|
802
|
+
const tab = await chrome.tabs.get(tabId);
|
|
803
|
+
if (tab.status === "complete") return;
|
|
804
|
+
await delay(250, signal);
|
|
805
|
+
}
|
|
806
|
+
throw new Error("Page load timeout");
|
|
807
|
+
}
|
|
808
|
+
async function evaluatePage(target, fn, args, signal) {
|
|
809
|
+
const result = await sendCommand(
|
|
810
|
+
target,
|
|
811
|
+
"Runtime.evaluate",
|
|
812
|
+
{
|
|
813
|
+
expression: `(${fn.toString()})(${JSON.stringify(args ?? null)})`,
|
|
814
|
+
returnByValue: true
|
|
815
|
+
},
|
|
816
|
+
signal
|
|
817
|
+
);
|
|
818
|
+
return result.result?.value;
|
|
819
|
+
}
|
|
820
|
+
function watchNetwork(target) {
|
|
821
|
+
const pending = /* @__PURE__ */ new Set();
|
|
822
|
+
let changedAt = Date.now();
|
|
823
|
+
const listener = (source, method, params) => {
|
|
824
|
+
if (source.tabId !== target.tabId) return;
|
|
825
|
+
if (method === "Network.requestWillBeSent" && ["Document", "XHR", "Fetch", "Stylesheet", "Font"].includes(params.type)) {
|
|
826
|
+
pending.add(params.requestId);
|
|
827
|
+
changedAt = Date.now();
|
|
828
|
+
} else if (["Network.loadingFinished", "Network.loadingFailed"].includes(method) && pending.delete(params.requestId))
|
|
829
|
+
changedAt = Date.now();
|
|
830
|
+
};
|
|
831
|
+
chrome.debugger.onEvent.addListener(listener);
|
|
832
|
+
return {
|
|
833
|
+
busy: () => pending.size > 0 || Date.now() - changedAt < 750,
|
|
834
|
+
reset: () => {
|
|
835
|
+
pending.clear();
|
|
836
|
+
changedAt = Date.now();
|
|
837
|
+
},
|
|
838
|
+
dispose: () => chrome.debugger.onEvent.removeListener(listener)
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async function preparePageForCapture(target, signal) {
|
|
842
|
+
await sendCommand(
|
|
843
|
+
target,
|
|
844
|
+
"Emulation.setFocusEmulationEnabled",
|
|
845
|
+
{ enabled: true },
|
|
846
|
+
signal
|
|
847
|
+
).catch((error) => {
|
|
848
|
+
throwIfAborted(signal);
|
|
308
849
|
});
|
|
309
850
|
}
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
851
|
+
function pageMetrics({ maxSeenY = 0 } = {}) {
|
|
852
|
+
function visible(el) {
|
|
853
|
+
if (!el?.getBoundingClientRect) return false;
|
|
854
|
+
const rect = el.getBoundingClientRect();
|
|
855
|
+
if (rect.width <= 2 || rect.height <= 2 || rect.top > maxSeenY + innerHeight)
|
|
856
|
+
return false;
|
|
857
|
+
for (let node = el; node instanceof Element; node = node.parentElement) {
|
|
858
|
+
const style = getComputedStyle(node);
|
|
859
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) < 0.05)
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
|
|
865
|
+
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])';
|
|
866
|
+
let loading = [...document.querySelectorAll(strong)].some(visible);
|
|
867
|
+
if (!loading)
|
|
868
|
+
loading = [
|
|
869
|
+
...document.querySelectorAll(".loading,.loader,.spinner,.skeleton")
|
|
870
|
+
].some((el) => {
|
|
871
|
+
if (!visible(el)) return false;
|
|
872
|
+
const words = (el.innerText || "").trim();
|
|
873
|
+
const rect = el.getBoundingClientRect();
|
|
874
|
+
return words.length < 160 && /^(loading\b|please wait\b|加载中|正在加载|読み込み中|로딩)/i.test(
|
|
875
|
+
words
|
|
876
|
+
) || !words && rect.width <= 256 && rect.height <= 256 && getComputedStyle(el).animationName !== "none";
|
|
333
877
|
});
|
|
334
|
-
|
|
878
|
+
const pendingImages = [...document.images].filter(
|
|
879
|
+
(img) => !img.complete && visible(img) && img.width * img.height > 4e3
|
|
880
|
+
).length;
|
|
881
|
+
const fontsPending = document.fonts?.status === "loading";
|
|
882
|
+
let hash = 0;
|
|
883
|
+
for (let i = 0; i < text.length; i += Math.max(1, Math.floor(text.length / 2e3)))
|
|
884
|
+
hash = Math.imul(hash, 31) + text.charCodeAt(i) | 0;
|
|
885
|
+
const height = Math.max(
|
|
886
|
+
document.body?.scrollHeight || 0,
|
|
887
|
+
document.documentElement.scrollHeight
|
|
888
|
+
);
|
|
889
|
+
return {
|
|
890
|
+
complete: document.readyState === "complete",
|
|
891
|
+
loading,
|
|
892
|
+
pendingImages,
|
|
893
|
+
fontsPending,
|
|
894
|
+
signature: [
|
|
895
|
+
location.href,
|
|
896
|
+
text.length,
|
|
897
|
+
hash,
|
|
898
|
+
height,
|
|
899
|
+
pendingImages,
|
|
900
|
+
fontsPending
|
|
901
|
+
].join("|")
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
async function waitForPageStable(target, options, signal, network, maxSeenY = 0) {
|
|
905
|
+
const start = Date.now();
|
|
906
|
+
let signature = "", stableSince = start, last;
|
|
907
|
+
while (Date.now() - start < options.readyTimeoutMs) {
|
|
908
|
+
throwIfAborted(signal);
|
|
909
|
+
last = await evaluatePage(target, pageMetrics, { maxSeenY }, signal);
|
|
910
|
+
if (!last) throw new Error("Could not inspect page readiness");
|
|
911
|
+
if (last.signature !== signature || !last.complete || last.loading || last.pendingImages || last.fontsPending || network?.busy())
|
|
912
|
+
stableSince = Date.now();
|
|
913
|
+
signature = last.signature;
|
|
914
|
+
if (Date.now() - stableSince >= 1e3) return "";
|
|
915
|
+
await delay(250, signal);
|
|
916
|
+
}
|
|
917
|
+
const reason = last?.loading ? "Page still shows loading UI" : "Page content is still changing or loading";
|
|
918
|
+
if (options.allowUnstable)
|
|
919
|
+
return `${reason}; captured after the readiness timeout at your request.`;
|
|
920
|
+
throw new Error(
|
|
921
|
+
`${reason}. Increase readiness wait, or choose Capture after timeout in Settings if this page is ready.`
|
|
922
|
+
);
|
|
335
923
|
}
|
|
336
924
|
function challengeError(name) {
|
|
337
925
|
const error = new Error(`Blocked by ${name} verification`);
|
|
@@ -341,122 +929,96 @@
|
|
|
341
929
|
function isChallengeError(error) {
|
|
342
930
|
return Boolean(error?.isChallenge);
|
|
343
931
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
var u=(location.href||"").toLowerCase();
|
|
354
|
-
var s=t+" "+b+" "+u;
|
|
355
|
-
var c="";
|
|
356
|
-
if(/just a moment|checking your browser|attention required|cf-browser-verification|challenges\\.cloudflare/.test(s)) c="Cloudflare";
|
|
357
|
-
else if(/verifying your browser|\u6B63\u5728\u9A8C\u8BC1\u60A8\u7684\u6D4F\u89C8\u5668|security checkpoint|\u5B89\u5168\u68C0\u67E5\u70B9/.test(s)) c="Vercel";
|
|
358
|
-
else if(/verify you are human|are you a robot|\u8BF7\u5B8C\u6210\u9A8C\u8BC1|\u4EBA\u673A\u9A8C\u8BC1|captcha/.test(s)) c="bot-check";
|
|
359
|
-
return {error:"",challenge:c};
|
|
360
|
-
})()`
|
|
932
|
+
function pageProbe() {
|
|
933
|
+
if (document.documentURI.startsWith("chrome-error://"))
|
|
934
|
+
return {
|
|
935
|
+
error: "Page failed to load (DNS or network error)",
|
|
936
|
+
challenge: ""
|
|
937
|
+
};
|
|
938
|
+
const shown = (selector) => [...document.querySelectorAll(selector)].some((el) => {
|
|
939
|
+
const r = el.getBoundingClientRect();
|
|
940
|
+
return r.width > 0 && r.height > 0 && el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true });
|
|
361
941
|
});
|
|
362
|
-
const
|
|
363
|
-
|
|
942
|
+
const title = document.title.trim().toLowerCase();
|
|
943
|
+
let challenge = "";
|
|
944
|
+
if (shown("#challenge-form,#challenge-stage,#cf-challenge-running") && /just a moment|checking your browser|attention required|verify/.test(
|
|
945
|
+
title
|
|
946
|
+
))
|
|
947
|
+
challenge = "Cloudflare";
|
|
948
|
+
else if (shown("#vercel-security-checkpoint,#challenge-container") && /security checkpoint|verifying your browser|安全检查点/.test(title))
|
|
949
|
+
challenge = "Vercel";
|
|
950
|
+
else if (/^(verify you are human|human verification|are you a robot|captcha|人机验证)[\s.!?—-]*$/i.test(
|
|
951
|
+
title
|
|
952
|
+
) && shown(
|
|
953
|
+
'iframe[src*="recaptcha"],iframe[src*="hcaptcha"],iframe[src*="challenges.cloudflare.com"],input[name*="captcha"],[data-sitekey]'
|
|
954
|
+
))
|
|
955
|
+
challenge = "bot-check";
|
|
956
|
+
return { error: "", challenge };
|
|
957
|
+
}
|
|
958
|
+
async function probePage(target, signal) {
|
|
959
|
+
return await evaluatePage(target, pageProbe, null, signal) || {
|
|
960
|
+
error: "",
|
|
961
|
+
challenge: ""
|
|
962
|
+
};
|
|
364
963
|
}
|
|
365
|
-
async function waitForVerification(tabId, shouldStop, timeoutMs = 18e4) {
|
|
964
|
+
async function waitForVerification(tabId, shouldStop, signal, timeoutMs = 18e4) {
|
|
366
965
|
const start = Date.now();
|
|
367
966
|
while (Date.now() - start < timeoutMs) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
await delay(2500);
|
|
967
|
+
throwIfAborted(signal);
|
|
968
|
+
if (shouldStop()) return false;
|
|
969
|
+
await delay(2500, signal);
|
|
372
970
|
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
373
|
-
if (!tab)
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
971
|
+
if (!tab) return false;
|
|
972
|
+
if (tab.status !== "complete") continue;
|
|
973
|
+
const target = { tabId };
|
|
974
|
+
let attached = false;
|
|
975
|
+
try {
|
|
976
|
+
await debuggerAttach(target, "1.3");
|
|
977
|
+
attached = true;
|
|
978
|
+
const result = await probePage(target, signal);
|
|
979
|
+
if (!result.challenge && !result.error) return true;
|
|
980
|
+
} catch {
|
|
981
|
+
throwIfAborted(signal);
|
|
982
|
+
} finally {
|
|
983
|
+
if (attached) await debuggerDetach(target).catch(() => {
|
|
984
|
+
});
|
|
383
985
|
}
|
|
384
986
|
}
|
|
385
987
|
return false;
|
|
386
988
|
}
|
|
387
989
|
async function focusTab(tabId) {
|
|
388
|
-
await chrome.tabs.update(tabId, { active: true })
|
|
389
|
-
});
|
|
390
|
-
try {
|
|
391
|
-
const tab = await chrome.tabs.get(tabId);
|
|
392
|
-
if (tab.windowId != null) {
|
|
393
|
-
await chrome.windows.update(tab.windowId, { focused: true });
|
|
394
|
-
}
|
|
395
|
-
} catch {
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
function autoScroll(target) {
|
|
399
|
-
return sendCommand(target, "Runtime.evaluate", {
|
|
400
|
-
awaitPromise: true,
|
|
401
|
-
expression: `new Promise(function(resolve){
|
|
402
|
-
var distance=600, scrolled=0, guard=0;
|
|
403
|
-
var maxHeight=Math.min(document.body?document.body.scrollHeight:0, 20000);
|
|
404
|
-
var timer=setInterval(function(){
|
|
405
|
-
window.scrollBy(0, distance);
|
|
406
|
-
scrolled+=distance; guard+=1;
|
|
407
|
-
var reached=scrolled>=Math.max(maxHeight, document.body?document.body.scrollHeight:0);
|
|
408
|
-
if(reached || guard>60){
|
|
409
|
-
clearInterval(timer);
|
|
410
|
-
setTimeout(function(){ window.scrollTo(0,0); setTimeout(resolve, 400); }, 300);
|
|
411
|
-
}
|
|
412
|
-
}, 120);
|
|
413
|
-
})`
|
|
414
|
-
}).catch(() => void 0);
|
|
990
|
+
const tab = await chrome.tabs.update(tabId, { active: true });
|
|
991
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
415
992
|
}
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
el:el,
|
|
446
|
-
opacity:el.style.getPropertyValue('opacity'),
|
|
447
|
-
opacityPriority:el.style.getPropertyPriority('opacity'),
|
|
448
|
-
transform:el.style.getPropertyValue('transform'),
|
|
449
|
-
transformPriority:el.style.getPropertyPriority('transform')
|
|
450
|
-
});
|
|
451
|
-
el.style.setProperty('opacity','1','important');
|
|
452
|
-
el.style.setProperty('transform','none','important');
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if(state) window.__zuckerExtRevealState=state;
|
|
457
|
-
}catch(e){}
|
|
458
|
-
})()`
|
|
459
|
-
}).catch(() => void 0);
|
|
993
|
+
async function autoScroll(target, signal) {
|
|
994
|
+
let maxSeenY = 0;
|
|
995
|
+
for (let step = 0; step < 60; step++) {
|
|
996
|
+
const position = await evaluatePage(
|
|
997
|
+
target,
|
|
998
|
+
() => {
|
|
999
|
+
window.scrollBy({ top: 600, behavior: "instant" });
|
|
1000
|
+
return {
|
|
1001
|
+
y: scrollY,
|
|
1002
|
+
bottom: scrollY + innerHeight >= Math.max(
|
|
1003
|
+
document.documentElement.scrollHeight,
|
|
1004
|
+
document.body?.scrollHeight || 0
|
|
1005
|
+
) - 1
|
|
1006
|
+
};
|
|
1007
|
+
},
|
|
1008
|
+
null,
|
|
1009
|
+
signal
|
|
1010
|
+
);
|
|
1011
|
+
maxSeenY = Math.max(maxSeenY, position.y);
|
|
1012
|
+
await delay(120, signal);
|
|
1013
|
+
if (position.bottom) break;
|
|
1014
|
+
}
|
|
1015
|
+
await evaluatePage(
|
|
1016
|
+
target,
|
|
1017
|
+
() => window.scrollTo({ left: 0, top: 0, behavior: "instant" }),
|
|
1018
|
+
null,
|
|
1019
|
+
signal
|
|
1020
|
+
);
|
|
1021
|
+
return maxSeenY;
|
|
460
1022
|
}
|
|
461
1023
|
async function getScrollPosition(target) {
|
|
462
1024
|
const result = await sendCommand(target, "Runtime.evaluate", {
|
|
@@ -466,71 +1028,91 @@
|
|
|
466
1028
|
const value = result.result?.value;
|
|
467
1029
|
return value || { x: 0, y: 0 };
|
|
468
1030
|
}
|
|
1031
|
+
function hidePageCaptureDecorations() {
|
|
1032
|
+
const hosts = [...document.querySelectorAll("browser-mcp-container")];
|
|
1033
|
+
window.__zuckerExtCaptureDecorations = hosts.map((el) => ({
|
|
1034
|
+
el,
|
|
1035
|
+
display: el.style.getPropertyValue("display"),
|
|
1036
|
+
priority: el.style.getPropertyPriority("display")
|
|
1037
|
+
}));
|
|
1038
|
+
hosts.forEach((el) => el.style.setProperty("display", "none", "important"));
|
|
1039
|
+
return {
|
|
1040
|
+
width: innerWidth,
|
|
1041
|
+
height: innerHeight,
|
|
1042
|
+
dpr: devicePixelRatio || 1
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function showPageCaptureDecorations() {
|
|
1046
|
+
for (const saved of window.__zuckerExtCaptureDecorations || []) {
|
|
1047
|
+
if (saved.display)
|
|
1048
|
+
saved.el.style.setProperty("display", saved.display, saved.priority);
|
|
1049
|
+
else saved.el.style.removeProperty("display");
|
|
1050
|
+
}
|
|
1051
|
+
delete window.__zuckerExtCaptureDecorations;
|
|
1052
|
+
}
|
|
1053
|
+
function suppressCaptureDecorations(target, signal) {
|
|
1054
|
+
return sendCommand(
|
|
1055
|
+
target,
|
|
1056
|
+
"Runtime.evaluate",
|
|
1057
|
+
{ expression: `(${hidePageCaptureDecorations.toString()})()` },
|
|
1058
|
+
signal
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
function restoreCaptureDecorations(target) {
|
|
1062
|
+
return sendCommand(target, "Runtime.evaluate", {
|
|
1063
|
+
expression: `(${showPageCaptureDecorations.toString()})()`
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
469
1066
|
function restorePageAfterQuick(target, position) {
|
|
470
1067
|
const x = Number(position?.x) || 0;
|
|
471
1068
|
const y = Number(position?.y) || 0;
|
|
472
1069
|
return sendCommand(target, "Runtime.evaluate", {
|
|
473
|
-
expression: `(
|
|
474
|
-
try{
|
|
475
|
-
var state=window.__zuckerExtRevealState;
|
|
476
|
-
if(state){
|
|
477
|
-
if(state.style&&state.style.remove) state.style.remove();
|
|
478
|
-
(state.aos||[]).forEach(function(el){ el.classList.remove('aos-animate'); });
|
|
479
|
-
(state.inline||[]).forEach(function(saved){
|
|
480
|
-
if(saved.opacity) saved.el.style.setProperty('opacity',saved.opacity,saved.opacityPriority||'');
|
|
481
|
-
else saved.el.style.removeProperty('opacity');
|
|
482
|
-
if(saved.transform) saved.el.style.setProperty('transform',saved.transform,saved.transformPriority||'');
|
|
483
|
-
else saved.el.style.removeProperty('transform');
|
|
484
|
-
});
|
|
485
|
-
delete window.__zuckerExtRevealState;
|
|
486
|
-
}
|
|
487
|
-
}catch(e){}
|
|
488
|
-
window.scrollTo(${x}, ${y});
|
|
489
|
-
})()`
|
|
1070
|
+
expression: `window.scrollTo({ left: ${x}, top: ${y}, behavior: "instant" })`
|
|
490
1071
|
});
|
|
491
1072
|
}
|
|
492
1073
|
|
|
493
|
-
// src/engine.
|
|
494
|
-
var LOAD_TIMEOUT_MS = 3e4;
|
|
495
|
-
var SAME_HOST_MIN_GAP_MS = 1800;
|
|
496
|
-
var DEFAULT_TEXTS = {
|
|
497
|
-
alreadyRunning: "Another capture is already running",
|
|
498
|
-
cannotQuick: "This page cannot be captured. Open an http(s) website first.",
|
|
499
|
-
challengeBeforeQuick: (challenge) => `Complete the ${challenge} verification before capturing this page.`,
|
|
500
|
-
quickFolder: "Quick Captures"
|
|
501
|
-
};
|
|
1074
|
+
// src/engine.js
|
|
502
1075
|
function initExtension(config) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
1076
|
+
if (!Number.isSafeInteger(config.freeDailyLimit) || config.freeDailyLimit < 0)
|
|
1077
|
+
throw new RangeError("freeDailyLimit must be an explicit non-negative integer");
|
|
1078
|
+
const normalizeSettings = (defaults, input) => {
|
|
1079
|
+
const settings = config.normalizeSettings ? config.normalizeSettings(defaults, input) : { ...defaults, ...input && typeof input === "object" && !Array.isArray(input) ? input : {} };
|
|
1080
|
+
return {
|
|
1081
|
+
...settings,
|
|
1082
|
+
delayMs: Math.min(1e4, Math.max(0, Number(settings.delayMs) || 0)),
|
|
1083
|
+
readyTimeoutMs: Math.min(6e4, Math.max(5e3, Number(settings.readyTimeoutMs) || 25e3)),
|
|
1084
|
+
allowUnstable: settings.allowUnstable === true,
|
|
1085
|
+
ignoreChallenge: settings.ignoreChallenge === true,
|
|
1086
|
+
expandScrollAreas: settings.expandScrollAreas !== false
|
|
1087
|
+
};
|
|
1088
|
+
};
|
|
1089
|
+
const scrollCapture = config.scrollCapture || scroll_capture_exports;
|
|
1090
|
+
const texts = {
|
|
1091
|
+
alreadyRunning: "Another capture is already running",
|
|
1092
|
+
cannotQuick: "Open an http(s) website first.",
|
|
1093
|
+
challengeBeforeQuick: (name) => `Complete the ${name} verification, or enable Capture verification page in Settings if this is a normal page.`,
|
|
1094
|
+
quickFolder: "Quick Captures",
|
|
1095
|
+
...config.texts
|
|
1096
|
+
};
|
|
507
1097
|
const prefix = config.messagePrefix;
|
|
508
|
-
const
|
|
1098
|
+
const BATCH_KEY = `${config.product}_batch_state`, QUICK_KEY = `${config.product}_quick_state`;
|
|
1099
|
+
const USAGE_KEY = `${config.product}_free_usage`, TAB_KEY = `${config.product}_owned_tab`;
|
|
1100
|
+
const licensing = createLicensing(
|
|
1101
|
+
config.product,
|
|
1102
|
+
config.brand,
|
|
1103
|
+
config.licensing
|
|
1104
|
+
);
|
|
509
1105
|
const downloader = createDownloader(
|
|
510
|
-
`
|
|
1106
|
+
`Prepare local ${config.brand} downloads.`,
|
|
1107
|
+
config.downloads
|
|
511
1108
|
);
|
|
1109
|
+
let activeRun = null;
|
|
512
1110
|
let batch = idleBatch();
|
|
513
|
-
let restorePromise
|
|
514
|
-
let quickRunning = false;
|
|
515
|
-
chrome.runtime.onInstalled.addListener(() => {
|
|
516
|
-
const previousRestore = restorePromise;
|
|
517
|
-
restorePromise = (async () => {
|
|
518
|
-
await previousRestore.catch(() => {
|
|
519
|
-
});
|
|
520
|
-
batch = idleBatch();
|
|
521
|
-
await storageRemove(BATCH_KEY);
|
|
522
|
-
setBadge("");
|
|
523
|
-
})();
|
|
524
|
-
});
|
|
525
|
-
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
526
|
-
if (message?.target === "offscreen") {
|
|
527
|
-
return false;
|
|
528
|
-
}
|
|
529
|
-
handleMessage(message).then(sendResponse).catch((error) => sendResponse({ ok: false, error: normalizeError(error) }));
|
|
530
|
-
return true;
|
|
531
|
-
});
|
|
1111
|
+
let restorePromise;
|
|
532
1112
|
function idleBatch() {
|
|
533
1113
|
return {
|
|
1114
|
+
version: 2,
|
|
1115
|
+
id: crypto.randomUUID(),
|
|
534
1116
|
running: false,
|
|
535
1117
|
cancelRequested: false,
|
|
536
1118
|
startedAt: null,
|
|
@@ -539,417 +1121,777 @@
|
|
|
539
1121
|
done: 0,
|
|
540
1122
|
success: 0,
|
|
541
1123
|
failed: 0,
|
|
1124
|
+
cancelled: 0,
|
|
542
1125
|
current: "",
|
|
543
1126
|
freeLimited: false,
|
|
544
1127
|
needVerify: null,
|
|
545
|
-
|
|
546
|
-
settings: { ...config.defaultSettings },
|
|
1128
|
+
settings: normalizeSettings(config.defaultSettings),
|
|
547
1129
|
pro: false,
|
|
548
1130
|
items: []
|
|
549
1131
|
};
|
|
550
1132
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
return cancelBatch();
|
|
562
|
-
case `${prefix}_CLEAR_BATCH`:
|
|
563
|
-
return clearBatch();
|
|
564
|
-
case `${prefix}_SKIP_VERIFY`:
|
|
565
|
-
batch.skipVerify = true;
|
|
566
|
-
await persistBatch();
|
|
567
|
-
return { ok: true };
|
|
568
|
-
case `${prefix}_STATE`:
|
|
569
|
-
return { ok: true, batch: publicBatch(), quota: await getQuota() };
|
|
570
|
-
case `${prefix}_ACTIVATE`:
|
|
571
|
-
await licensing.activate(message.code);
|
|
572
|
-
return { ok: true, quota: await getQuota() };
|
|
573
|
-
case `${prefix}_CLEAR_LICENSE`:
|
|
574
|
-
await licensing.clear();
|
|
575
|
-
return { ok: true, quota: await getQuota() };
|
|
576
|
-
default:
|
|
577
|
-
return { ok: false, error: "Unknown message" };
|
|
578
|
-
}
|
|
1133
|
+
function reserveRun(restoring = false) {
|
|
1134
|
+
if (activeRun || !restoring && batch.running) return null;
|
|
1135
|
+
activeRun = {
|
|
1136
|
+
controller: new AbortController(),
|
|
1137
|
+
job: null,
|
|
1138
|
+
skipVerify: false,
|
|
1139
|
+
windowId: null,
|
|
1140
|
+
idleTabId: null
|
|
1141
|
+
};
|
|
1142
|
+
return activeRun;
|
|
579
1143
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1144
|
+
function releaseRun(run) {
|
|
1145
|
+
if (activeRun === run) {
|
|
1146
|
+
activeRun = null;
|
|
1147
|
+
setBadge("");
|
|
584
1148
|
}
|
|
585
|
-
const data = await storageGet(USAGE_KEY);
|
|
586
|
-
const usage = data[USAGE_KEY];
|
|
587
|
-
const today = todayString();
|
|
588
|
-
const used = usage && usage.date === today ? usage.used || 0 : 0;
|
|
589
|
-
return { pro: false, limit: freeDailyLimit, used, remaining: Math.max(0, freeDailyLimit - used) };
|
|
590
1149
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1150
|
+
function recount(job) {
|
|
1151
|
+
job.success = job.items.filter((i) => i.status === "success").length;
|
|
1152
|
+
job.failed = job.items.filter((i) => i.status === "failed").length;
|
|
1153
|
+
job.cancelled = job.items.filter((i) => i.status === "cancelled").length;
|
|
1154
|
+
job.done = job.success + job.failed + job.cancelled;
|
|
1155
|
+
}
|
|
1156
|
+
function publicBatch() {
|
|
1157
|
+
return {
|
|
1158
|
+
...batch,
|
|
1159
|
+
busy: Boolean(activeRun),
|
|
1160
|
+
// Marked running with no live controller: a final write failed and left
|
|
1161
|
+
// downloads unconfirmed. The popup must stay usable in this state.
|
|
1162
|
+
stalled: Boolean(batch.running && !activeRun),
|
|
1163
|
+
items: batch.items.map(
|
|
1164
|
+
({ url, index, status, fileName, error, warning }) => ({
|
|
1165
|
+
url,
|
|
1166
|
+
index,
|
|
1167
|
+
status,
|
|
1168
|
+
fileName,
|
|
1169
|
+
error,
|
|
1170
|
+
warning
|
|
1171
|
+
})
|
|
1172
|
+
)
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
const persist = (job) => storageSet({ [BATCH_KEY]: job });
|
|
1176
|
+
function usageFrom(data) {
|
|
594
1177
|
const usage = data[USAGE_KEY];
|
|
595
|
-
|
|
596
|
-
await storageSet({ [USAGE_KEY]: { date: today, used: used + count } });
|
|
1178
|
+
return usage?.date === todayString() && Number.isSafeInteger(usage.used) && usage.used >= 0 ? usage.used : 0;
|
|
597
1179
|
}
|
|
598
|
-
async function
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (!quota.pro && quota.remaining <= 0) {
|
|
1180
|
+
async function getQuota(cachedOnly = false, force = false) {
|
|
1181
|
+
const status = await licensing.getStatus({ cachedOnly, force });
|
|
1182
|
+
const license = {
|
|
1183
|
+
licenseStatus: status.state,
|
|
1184
|
+
licenseMessage: status.message || "",
|
|
1185
|
+
licenseRefreshing: Boolean(status.refreshing),
|
|
1186
|
+
hasLicense: status.state !== "free"
|
|
1187
|
+
};
|
|
1188
|
+
if (status.entitlement)
|
|
608
1189
|
return {
|
|
609
|
-
|
|
610
|
-
|
|
1190
|
+
...license,
|
|
1191
|
+
pro: true,
|
|
1192
|
+
expiresAt: status.entitlement.expiresAt,
|
|
1193
|
+
limit: null,
|
|
1194
|
+
used: 0,
|
|
1195
|
+
remaining: null
|
|
611
1196
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
pro:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
status: "pending",
|
|
629
|
-
fileName: "",
|
|
630
|
-
error: "",
|
|
631
|
-
warning: "",
|
|
632
|
-
usageRecorded: false
|
|
633
|
-
}))
|
|
1197
|
+
if (status.state !== "free")
|
|
1198
|
+
return {
|
|
1199
|
+
...license,
|
|
1200
|
+
pro: false,
|
|
1201
|
+
limit: null,
|
|
1202
|
+
used: 0,
|
|
1203
|
+
remaining: null
|
|
1204
|
+
};
|
|
1205
|
+
const used = usageFrom(await storageGet(USAGE_KEY));
|
|
1206
|
+
const limit = config.freeDailyLimit;
|
|
1207
|
+
return {
|
|
1208
|
+
...license,
|
|
1209
|
+
pro: false,
|
|
1210
|
+
limit,
|
|
1211
|
+
used,
|
|
1212
|
+
remaining: Math.max(0, limit - used)
|
|
634
1213
|
};
|
|
635
|
-
await persistBatch();
|
|
636
|
-
runBatch(options).catch((error) => {
|
|
637
|
-
batch.running = false;
|
|
638
|
-
batch.current = normalizeError(error);
|
|
639
|
-
persistBatch();
|
|
640
|
-
});
|
|
641
|
-
return { ok: true, batch: publicBatch(), quota: await getQuota() };
|
|
642
1214
|
}
|
|
643
|
-
|
|
644
|
-
if (
|
|
645
|
-
|
|
1215
|
+
function assertQuota(quota) {
|
|
1216
|
+
if (quota.pro) return;
|
|
1217
|
+
if (quota.hasLicense)
|
|
1218
|
+
throw new Error(
|
|
1219
|
+
quota.licenseMessage || "Checking your Pro license. Use Recheck Pro in License and retry."
|
|
1220
|
+
);
|
|
1221
|
+
if (!quota.remaining)
|
|
1222
|
+
throw new Error(
|
|
1223
|
+
`Daily free limit reached (${quota.limit} pages/day). Already purchased Pro? Enter your original activation code in License to restore unlimited exports.`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
async function commitResult(job, item, status, error = "", quick = false) {
|
|
1227
|
+
const previous = {
|
|
1228
|
+
status: item.status,
|
|
1229
|
+
error: item.error,
|
|
1230
|
+
usageRecorded: item.usageRecorded
|
|
1231
|
+
};
|
|
1232
|
+
let used;
|
|
1233
|
+
if (status === "success" && item.chargeFree && !item.usageRecorded) {
|
|
1234
|
+
used = usageFrom(await storageGet(USAGE_KEY)) + 1;
|
|
646
1235
|
}
|
|
647
|
-
|
|
648
|
-
|
|
1236
|
+
item.status = status;
|
|
1237
|
+
item.error = error;
|
|
1238
|
+
if (used !== void 0) item.usageRecorded = true;
|
|
1239
|
+
if (!quick) recount(job);
|
|
1240
|
+
try {
|
|
1241
|
+
await storageSet({
|
|
1242
|
+
[quick ? QUICK_KEY : BATCH_KEY]: quick ? null : job,
|
|
1243
|
+
...used !== void 0 ? { [USAGE_KEY]: { date: todayString(), used } } : {}
|
|
1244
|
+
});
|
|
1245
|
+
} catch (error2) {
|
|
1246
|
+
Object.assign(item, previous);
|
|
1247
|
+
if (!quick) recount(job);
|
|
1248
|
+
throw error2;
|
|
649
1249
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1250
|
+
}
|
|
1251
|
+
function makeItem(url, index) {
|
|
1252
|
+
return {
|
|
1253
|
+
url,
|
|
1254
|
+
index,
|
|
1255
|
+
status: "pending",
|
|
1256
|
+
fileName: "",
|
|
1257
|
+
error: "",
|
|
1258
|
+
warning: "",
|
|
1259
|
+
usageRecorded: false
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
async function recoverQuick(signal) {
|
|
1263
|
+
const quick = (await storageGet(QUICK_KEY))[QUICK_KEY];
|
|
1264
|
+
if (!quick) return;
|
|
1265
|
+
if (quick.downloadId == null && !quick.downloadUrl) {
|
|
1266
|
+
await storageRemove(QUICK_KEY);
|
|
1267
|
+
return;
|
|
653
1268
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1269
|
+
let recovered = false;
|
|
1270
|
+
try {
|
|
1271
|
+
await downloader.recover(quick, signal);
|
|
1272
|
+
recovered = true;
|
|
1273
|
+
} catch {
|
|
1274
|
+
await storageRemove(QUICK_KEY);
|
|
1275
|
+
}
|
|
1276
|
+
if (recovered) await commitResult(null, quick, "success", "", true);
|
|
1277
|
+
}
|
|
1278
|
+
async function startBatch(urls, settings) {
|
|
1279
|
+
const run = reserveRun();
|
|
1280
|
+
if (!run) return { ok: false, error: texts.alreadyRunning };
|
|
1281
|
+
try {
|
|
1282
|
+
await recoverQuick(run.controller.signal);
|
|
1283
|
+
const normalized = normalizeUrls(urls);
|
|
1284
|
+
if (!normalized.length) throw new Error("No valid http(s) URLs");
|
|
1285
|
+
const quota = await withAbort(getQuota(), run.controller.signal);
|
|
1286
|
+
throwIfAborted(run.controller.signal);
|
|
1287
|
+
assertQuota(quota);
|
|
1288
|
+
const accepted = quota.pro ? normalized : normalized.slice(0, quota.remaining);
|
|
1289
|
+
batch = {
|
|
1290
|
+
...idleBatch(),
|
|
1291
|
+
running: true,
|
|
1292
|
+
startedAt: Date.now(),
|
|
1293
|
+
folder: `${config.brand}/${folderNameFor(accepted)}-${crypto.randomUUID().slice(0, 6)}`,
|
|
1294
|
+
total: accepted.length,
|
|
1295
|
+
freeLimited: accepted.length < normalized.length,
|
|
1296
|
+
settings: normalizeSettings(config.defaultSettings, settings),
|
|
1297
|
+
pro: quota.pro,
|
|
1298
|
+
items: accepted.map((url, index) => makeItem(url, index + 1))
|
|
659
1299
|
};
|
|
1300
|
+
run.job = batch;
|
|
1301
|
+
await persist(batch);
|
|
1302
|
+
void runBatch(batch, run);
|
|
1303
|
+
return { ok: true, batch: publicBatch(), quota };
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
releaseRun(run);
|
|
1306
|
+
throw error;
|
|
660
1307
|
}
|
|
661
|
-
|
|
662
|
-
|
|
1308
|
+
}
|
|
1309
|
+
async function runBatch(job, run, { keepLock = false } = {}) {
|
|
1310
|
+
const signal = run.controller.signal;
|
|
1311
|
+
const lastHostAt = /* @__PURE__ */ new Map();
|
|
663
1312
|
try {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1313
|
+
for (const item of job.items) {
|
|
1314
|
+
if (["success", "failed", "cancelled"].includes(item.status))
|
|
1315
|
+
continue;
|
|
1316
|
+
if (item.downloadId != null || item.downloadUrl) {
|
|
1317
|
+
let error2;
|
|
1318
|
+
try {
|
|
1319
|
+
await downloader.recover(item, signal);
|
|
1320
|
+
} catch (failure) {
|
|
1321
|
+
error2 = failure;
|
|
1322
|
+
}
|
|
1323
|
+
await commitResult(
|
|
1324
|
+
job,
|
|
1325
|
+
item,
|
|
1326
|
+
error2 ? signal.aborted || error2.message === "Cancelled" ? "cancelled" : "failed" : "success",
|
|
1327
|
+
error2 ? normalizeError(error2) : ""
|
|
1328
|
+
);
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
throwIfAborted(signal);
|
|
1332
|
+
const quota = await withAbort(getQuota(), signal);
|
|
1333
|
+
assertQuota(quota);
|
|
1334
|
+
const host = hostOf(item.url), previous = lastHostAt.get(host) || 0;
|
|
1335
|
+
await delay(Math.max(0, (config.sameHostGapMs ?? 1800) - (Date.now() - previous)), signal);
|
|
1336
|
+
lastHostAt.set(host, Date.now());
|
|
1337
|
+
item.chargeFree = !quota.pro;
|
|
1338
|
+
item.status = "running";
|
|
1339
|
+
job.current = item.url;
|
|
1340
|
+
setBadge(`${job.done + 1}/${job.total}`);
|
|
1341
|
+
await persist(job);
|
|
1342
|
+
let error;
|
|
1343
|
+
try {
|
|
1344
|
+
await captureUrl(item, job, run);
|
|
1345
|
+
} catch (failure) {
|
|
1346
|
+
error = failure;
|
|
1347
|
+
}
|
|
1348
|
+
await commitResult(
|
|
1349
|
+
job,
|
|
1350
|
+
item,
|
|
1351
|
+
error ? signal.aborted || error.message === "Cancelled" ? "cancelled" : "failed" : "success",
|
|
1352
|
+
error ? normalizeError(error) : ""
|
|
1353
|
+
);
|
|
668
1354
|
}
|
|
669
|
-
setBadge("OK");
|
|
670
|
-
setTimeout(() => setBadge(""), 4e3);
|
|
671
|
-
return { ok: true, result, quota: await getQuota() };
|
|
672
1355
|
} catch (error) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1356
|
+
const message = normalizeError(error);
|
|
1357
|
+
job.current = message;
|
|
1358
|
+
if (!signal.aborted && !job.cancelRequested) {
|
|
1359
|
+
for (const item of job.items)
|
|
1360
|
+
if (item.status === "pending") {
|
|
1361
|
+
item.status = "failed";
|
|
1362
|
+
item.error = message;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
676
1365
|
} finally {
|
|
677
|
-
|
|
1366
|
+
if (signal.aborted || job.cancelRequested) {
|
|
1367
|
+
job.cancelRequested = true;
|
|
1368
|
+
for (const item of job.items)
|
|
1369
|
+
if (["pending", "running"].includes(item.status))
|
|
1370
|
+
item.status = "cancelled";
|
|
1371
|
+
job.current = "Stopped";
|
|
1372
|
+
} else if (job.items.every(
|
|
1373
|
+
(i) => ["success", "failed", "cancelled"].includes(i.status)
|
|
1374
|
+
))
|
|
1375
|
+
job.current = "Done";
|
|
1376
|
+
job.running = job.items.some((item) => item.status === "downloading");
|
|
1377
|
+
if (job.running)
|
|
1378
|
+
job.current = "Some downloads could not be confirmed. Press Stop to reconcile them.";
|
|
1379
|
+
job.needVerify = null;
|
|
1380
|
+
recount(job);
|
|
1381
|
+
try {
|
|
1382
|
+
await persist(job);
|
|
1383
|
+
} catch {
|
|
1384
|
+
console.warn(`${config.brand}: could not save final task state`);
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
await closeBatchWindow(run);
|
|
1388
|
+
} finally {
|
|
1389
|
+
if (!keepLock) releaseRun(run);
|
|
1390
|
+
}
|
|
678
1391
|
}
|
|
679
1392
|
}
|
|
680
|
-
async function
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
1393
|
+
async function quickCapture(tabId, settings) {
|
|
1394
|
+
const run = reserveRun();
|
|
1395
|
+
if (!run) return { ok: false, error: texts.alreadyRunning };
|
|
1396
|
+
const signal = run.controller.signal;
|
|
684
1397
|
try {
|
|
685
|
-
await
|
|
686
|
-
|
|
687
|
-
await
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
1398
|
+
await recoverQuick(signal);
|
|
1399
|
+
if (!Number.isInteger(tabId)) throw new Error("No active web page");
|
|
1400
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1401
|
+
if (!/^https?:\/\//i.test(tab?.url || ""))
|
|
1402
|
+
throw new Error(texts.cannotQuick);
|
|
1403
|
+
const quota = await withAbort(getQuota(), signal);
|
|
1404
|
+
assertQuota(quota);
|
|
1405
|
+
const item = { ...makeItem(tab.url, 1), chargeFree: !quota.pro };
|
|
1406
|
+
const options = normalizeSettings(config.defaultSettings, settings);
|
|
1407
|
+
await storageSet({ [QUICK_KEY]: item });
|
|
1408
|
+
const visibleOutput = await config.captureQuick?.({
|
|
1409
|
+
tab,
|
|
1410
|
+
options,
|
|
1411
|
+
signal
|
|
1412
|
+
});
|
|
1413
|
+
if (visibleOutput)
|
|
1414
|
+
return await deliverQuick(item, visibleOutput, signal);
|
|
1415
|
+
const target = { tabId };
|
|
1416
|
+
let attached = false, position = null;
|
|
1417
|
+
const network = watchNetwork(target);
|
|
1418
|
+
const ctx = makeContext(
|
|
1419
|
+
"quick",
|
|
1420
|
+
tabId,
|
|
1421
|
+
item.url,
|
|
1422
|
+
options,
|
|
1423
|
+
target,
|
|
1424
|
+
signal
|
|
1425
|
+
);
|
|
1426
|
+
try {
|
|
1427
|
+
await debuggerAttach(target, "1.3");
|
|
1428
|
+
attached = true;
|
|
1429
|
+
if ((config.captureScope?.(options) || "full") === "full")
|
|
1430
|
+
position = await getScrollPosition(target);
|
|
1431
|
+
await config.beforeQuick?.(ctx, tab);
|
|
1432
|
+
await sendCommand(target, "Network.enable", {}, signal);
|
|
1433
|
+
if ((config.captureScope?.(options) || "full") === "full") {
|
|
1434
|
+
await preparePageForCapture(target, signal);
|
|
1435
|
+
}
|
|
1436
|
+
await suppressCaptureDecorations(target, signal);
|
|
1437
|
+
await prepareCapture(target, item, options, run, network, "quick");
|
|
1438
|
+
const output = await capturePrepared(ctx, options, run, network);
|
|
1439
|
+
throwIfAborted(signal);
|
|
1440
|
+
return await deliverQuick(item, output, signal);
|
|
1441
|
+
} finally {
|
|
1442
|
+
network.dispose();
|
|
1443
|
+
if (attached) {
|
|
1444
|
+
await config.afterQuick?.(ctx).catch(() => {
|
|
1445
|
+
});
|
|
1446
|
+
await restoreCaptureDecorations(target).catch(() => {
|
|
1447
|
+
});
|
|
1448
|
+
if (position && await isCurrentQuickPage(ctx))
|
|
1449
|
+
await restorePageAfterQuick(target, position).catch(() => {
|
|
1450
|
+
});
|
|
1451
|
+
await debuggerDetach(target).catch(() => {
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
695
1454
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
const saved = (await storageGet(QUICK_KEY))[QUICK_KEY];
|
|
1457
|
+
if (!saved?.downloadId && !saved?.downloadUrl)
|
|
1458
|
+
await storageRemove(QUICK_KEY);
|
|
1459
|
+
return {
|
|
1460
|
+
ok: false,
|
|
1461
|
+
error: normalizeError(error),
|
|
1462
|
+
quota: await getQuota(true)
|
|
1463
|
+
};
|
|
705
1464
|
} finally {
|
|
706
|
-
|
|
707
|
-
await restorePageAfterQuick(target, scrollPosition).catch(() => {
|
|
708
|
-
});
|
|
709
|
-
await debuggerDetach(target).catch(() => {
|
|
710
|
-
});
|
|
711
|
-
}
|
|
1465
|
+
releaseRun(run);
|
|
712
1466
|
}
|
|
713
1467
|
}
|
|
714
|
-
async function
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1468
|
+
async function deliverQuick(item, output, signal) {
|
|
1469
|
+
throwIfAborted(signal);
|
|
1470
|
+
item.warning = [item.warning, output.warning].filter(Boolean).join(" ");
|
|
1471
|
+
item.fileName = buildQuickFileName(
|
|
1472
|
+
config.brand,
|
|
1473
|
+
texts.quickFolder,
|
|
1474
|
+
item.url,
|
|
1475
|
+
output.extension
|
|
1476
|
+
);
|
|
1477
|
+
await saveOutput(
|
|
1478
|
+
item,
|
|
1479
|
+
output,
|
|
1480
|
+
signal,
|
|
1481
|
+
() => storageSet({ [QUICK_KEY]: item })
|
|
1482
|
+
);
|
|
1483
|
+
await commitResult(null, item, "success", "", true);
|
|
1484
|
+
return {
|
|
1485
|
+
ok: true,
|
|
1486
|
+
result: { fileName: item.fileName, warning: item.warning },
|
|
1487
|
+
quota: await getQuota(true)
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
async function isCurrentQuickPage(ctx) {
|
|
1491
|
+
if (ctx.mode !== "quick") return true;
|
|
1492
|
+
try {
|
|
1493
|
+
const current = await chrome.tabs.get(ctx.tabId);
|
|
1494
|
+
return current.url === ctx.url && (!current.pendingUrl || current.pendingUrl === ctx.url);
|
|
1495
|
+
} catch {
|
|
1496
|
+
return false;
|
|
719
1497
|
}
|
|
720
|
-
return { ok: true, batch: publicBatch() };
|
|
721
1498
|
}
|
|
722
|
-
async function
|
|
723
|
-
if (
|
|
724
|
-
|
|
1499
|
+
async function assertCurrentQuickPage(ctx) {
|
|
1500
|
+
if (!await isCurrentQuickPage(ctx))
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
"The page changed during capture. Return to the requested page and try again."
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
async function capturePrepared(ctx, options, run, network) {
|
|
1506
|
+
const expand = (config.captureScope?.(options) || "full") === "full" && options.expandScrollAreas;
|
|
1507
|
+
try {
|
|
1508
|
+
await assertCurrentQuickPage(ctx);
|
|
1509
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
1510
|
+
if (expand)
|
|
1511
|
+
await scrollCapture?.prepare(ctx, async () => {
|
|
1512
|
+
await assertCurrentQuickPage(ctx);
|
|
1513
|
+
const warning = await waitForPageStable(
|
|
1514
|
+
{ tabId: ctx.tabId },
|
|
1515
|
+
options,
|
|
1516
|
+
run.controller.signal,
|
|
1517
|
+
network
|
|
1518
|
+
);
|
|
1519
|
+
if (warning) warnings.add(warning);
|
|
1520
|
+
});
|
|
1521
|
+
await assertCurrentQuickPage(ctx);
|
|
1522
|
+
const output = await config.capture(ctx);
|
|
1523
|
+
await assertCurrentQuickPage(ctx);
|
|
1524
|
+
if (output.warning) warnings.add(output.warning);
|
|
1525
|
+
return { ...output, warning: [...warnings].join(" ") };
|
|
1526
|
+
} finally {
|
|
1527
|
+
if (expand) await scrollCapture?.restore(ctx);
|
|
725
1528
|
}
|
|
726
|
-
batch = idleBatch();
|
|
727
|
-
await storageRemove(BATCH_KEY);
|
|
728
|
-
setBadge("");
|
|
729
|
-
return { ok: true, batch: publicBatch() };
|
|
730
1529
|
}
|
|
731
|
-
async function
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1530
|
+
async function prepareCapture(target, item, options, run, network, mode = "batch") {
|
|
1531
|
+
const signal = run.controller.signal;
|
|
1532
|
+
const probe = await probePage(target, signal);
|
|
1533
|
+
if (probe.error) throw new Error(probe.error);
|
|
1534
|
+
if (probe.challenge && !options.ignoreChallenge)
|
|
1535
|
+
throw challengeError(probe.challenge);
|
|
1536
|
+
if (probe.challenge)
|
|
1537
|
+
item.warning = `Captured a ${probe.challenge} verification page at your request.`;
|
|
1538
|
+
const scope = config.captureScope?.(options) || "full";
|
|
1539
|
+
if (scope === "viewport" && mode === "quick") return;
|
|
1540
|
+
await delay(options.delayMs, signal);
|
|
1541
|
+
const before = await waitForPageStable(target, options, signal, network);
|
|
1542
|
+
const maxSeenY = scope === "full" ? await autoScroll(target, signal) : 0;
|
|
1543
|
+
const after = await waitForPageStable(
|
|
1544
|
+
target,
|
|
1545
|
+
options,
|
|
1546
|
+
signal,
|
|
1547
|
+
network,
|
|
1548
|
+
maxSeenY
|
|
1549
|
+
);
|
|
1550
|
+
item.warning = [
|
|
1551
|
+
...new Set([item.warning, before, after].filter(Boolean))
|
|
1552
|
+
].join(" ");
|
|
1553
|
+
}
|
|
1554
|
+
async function saveOutput(item, output, signal, save) {
|
|
1555
|
+
throwIfAborted(signal);
|
|
1556
|
+
await downloader.downloadViaBlob(
|
|
1557
|
+
output.base64,
|
|
1558
|
+
output.mime,
|
|
1559
|
+
item.fileName,
|
|
1560
|
+
{
|
|
1561
|
+
signal,
|
|
1562
|
+
onPrepared: async (url) => {
|
|
1563
|
+
item.downloadUrl = url;
|
|
1564
|
+
item.status = "downloading";
|
|
1565
|
+
await save();
|
|
1566
|
+
},
|
|
1567
|
+
onCreated: async (id) => {
|
|
1568
|
+
item.downloadId = id;
|
|
1569
|
+
await save();
|
|
764
1570
|
}
|
|
765
|
-
} catch (error) {
|
|
766
|
-
item.status = "failed";
|
|
767
|
-
item.error = normalizeError(error);
|
|
768
|
-
batch.failed += 1;
|
|
769
|
-
}
|
|
770
|
-
batch.done = batch.success + batch.failed;
|
|
771
|
-
await persistBatch();
|
|
772
|
-
if (shouldCharge) {
|
|
773
|
-
await recordUsage(1);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
batch.running = false;
|
|
777
|
-
batch.current = batch.cancelRequested ? "Stopped" : "Done";
|
|
778
|
-
batch.needVerify = null;
|
|
779
|
-
await persistBatch();
|
|
780
|
-
setBadge(batch.failed ? "ERR" : "OK");
|
|
781
|
-
setTimeout(() => setBadge(""), 4e3);
|
|
782
|
-
}
|
|
783
|
-
async function captureUrl(item, options) {
|
|
784
|
-
const maxChallengeRetry = 2;
|
|
785
|
-
let lastError;
|
|
786
|
-
for (let attempt = 0; attempt <= maxChallengeRetry; attempt += 1) {
|
|
787
|
-
if (batch.cancelRequested) {
|
|
788
|
-
throw new Error("Cancelled");
|
|
789
1571
|
}
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
async function captureUrl(item, job, run) {
|
|
1575
|
+
const signal = run.controller.signal;
|
|
1576
|
+
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
1577
|
+
throwIfAborted(signal);
|
|
790
1578
|
try {
|
|
791
|
-
return await captureOnce(item,
|
|
1579
|
+
return await captureOnce(item, job, run, attempt === 3);
|
|
792
1580
|
} catch (error) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (attempt < maxChallengeRetry) {
|
|
798
|
-
const backoff = 8e3 * (attempt + 1);
|
|
799
|
-
batch.current = `Rate-limited \u2014 cooling down ${backoff / 1e3}s (retry ${attempt + 1}/${maxChallengeRetry})`;
|
|
800
|
-
await delay(backoff);
|
|
801
|
-
}
|
|
1581
|
+
if (!isChallengeError(error) || attempt === 3) throw error;
|
|
1582
|
+
job.current = "Waiting before retrying verification\u2026";
|
|
1583
|
+
await persist(job);
|
|
1584
|
+
await delay(8e3 * (attempt + 1), signal);
|
|
802
1585
|
}
|
|
803
1586
|
}
|
|
804
|
-
if (isChallengeError(lastError) && !batch.cancelRequested) {
|
|
805
|
-
return await captureOnce(item, options, true);
|
|
806
|
-
}
|
|
807
|
-
throw lastError;
|
|
808
1587
|
}
|
|
809
|
-
async function
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1588
|
+
async function ensureBatchWindow(run) {
|
|
1589
|
+
if (Number.isInteger(run.windowId)) {
|
|
1590
|
+
const live = await chrome.windows.get(run.windowId).catch(() => null);
|
|
1591
|
+
if (live) return run.windowId;
|
|
1592
|
+
run.windowId = null;
|
|
1593
|
+
run.idleTabId = null;
|
|
814
1594
|
}
|
|
815
|
-
const
|
|
1595
|
+
const created = await chrome.windows.create({ url: "about:blank", focused: false }).catch(() => null);
|
|
1596
|
+
run.windowId = Number.isInteger(created?.id) ? created.id : null;
|
|
1597
|
+
run.idleTabId = created?.tabs?.[0]?.id ?? null;
|
|
1598
|
+
await saveTabLease(run);
|
|
1599
|
+
return run.windowId;
|
|
1600
|
+
}
|
|
1601
|
+
function saveTabLease(run, tabId = null) {
|
|
1602
|
+
return storageCall("session", "set", {
|
|
1603
|
+
[TAB_KEY]: {
|
|
1604
|
+
tabId,
|
|
1605
|
+
jobId: run.job?.id,
|
|
1606
|
+
windowId: run.windowId,
|
|
1607
|
+
idleTabId: run.idleTabId
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
async function closeIdleTab(lease) {
|
|
1612
|
+
if (!Number.isInteger(lease?.idleTabId)) return;
|
|
1613
|
+
const tab = await chrome.tabs.get(lease.idleTabId).catch(() => null);
|
|
1614
|
+
if (tab?.windowId === lease.windowId && tab.url === "about:blank" && (!tab.pendingUrl || tab.pendingUrl === "about:blank"))
|
|
1615
|
+
await chrome.tabs.remove(tab.id).catch(() => {
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
async function closeBatchWindow(run) {
|
|
1619
|
+
await closeIdleTab(run);
|
|
1620
|
+
run.windowId = null;
|
|
1621
|
+
run.idleTabId = null;
|
|
1622
|
+
await storageCall("session", "remove", TAB_KEY).catch(() => {
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
async function captureOnce(item, job, run, interactive) {
|
|
1626
|
+
const signal = run.controller.signal, options = job.settings;
|
|
1627
|
+
const windowId = await ensureBatchWindow(run);
|
|
1628
|
+
const tab = await chrome.tabs.create({
|
|
1629
|
+
url: "about:blank",
|
|
1630
|
+
active: interactive,
|
|
1631
|
+
...Number.isInteger(windowId) ? { windowId } : {}
|
|
1632
|
+
});
|
|
1633
|
+
const tabId = tab.id, target = { tabId };
|
|
816
1634
|
let attached = false;
|
|
1635
|
+
const network = watchNetwork(target);
|
|
817
1636
|
try {
|
|
818
|
-
await
|
|
819
|
-
|
|
820
|
-
if (batch.cancelRequested) {
|
|
821
|
-
throw new Error("Cancelled");
|
|
822
|
-
}
|
|
1637
|
+
await saveTabLease(run, tabId);
|
|
1638
|
+
throwIfAborted(signal);
|
|
823
1639
|
await debuggerAttach(target, "1.3");
|
|
824
1640
|
attached = true;
|
|
825
|
-
await sendCommand(target, "Page.enable");
|
|
826
|
-
await sendCommand(target, "
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1641
|
+
await sendCommand(target, "Page.enable", {}, signal);
|
|
1642
|
+
await sendCommand(target, "Network.enable", {}, signal);
|
|
1643
|
+
const ctx = makeContext(
|
|
1644
|
+
"batch",
|
|
1645
|
+
tabId,
|
|
1646
|
+
item.url,
|
|
1647
|
+
options,
|
|
1648
|
+
target,
|
|
1649
|
+
signal
|
|
1650
|
+
);
|
|
1651
|
+
if (config.beforeScroll) await config.beforeScroll(ctx);
|
|
1652
|
+
await preparePageForCapture(target, signal);
|
|
1653
|
+
await chrome.tabs.update(tabId, { url: item.url });
|
|
1654
|
+
await waitForLoad(tabId, 3e4, signal);
|
|
1655
|
+
await suppressCaptureDecorations(target, signal);
|
|
1656
|
+
const probe = await probePage(target, signal);
|
|
1657
|
+
if (probe.challenge && !options.ignoreChallenge && interactive) {
|
|
1658
|
+
await debuggerDetach(target);
|
|
837
1659
|
attached = false;
|
|
838
1660
|
await focusTab(tabId);
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const passed = await waitForVerification(
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
await
|
|
1661
|
+
job.needVerify = { url: item.url, challenge: probe.challenge };
|
|
1662
|
+
job.current = `Complete ${probe.challenge} verification in the opened tab\u2026`;
|
|
1663
|
+
run.skipVerify = false;
|
|
1664
|
+
await persist(job);
|
|
1665
|
+
const passed = await waitForVerification(
|
|
1666
|
+
tabId,
|
|
1667
|
+
() => run.skipVerify,
|
|
1668
|
+
signal
|
|
1669
|
+
);
|
|
1670
|
+
job.needVerify = null;
|
|
1671
|
+
job.current = item.url;
|
|
1672
|
+
await persist(job);
|
|
1673
|
+
if (!passed)
|
|
1674
|
+
throw challengeError(
|
|
1675
|
+
`${probe.challenge} verification not completed`
|
|
1676
|
+
);
|
|
851
1677
|
await debuggerAttach(target, "1.3");
|
|
852
1678
|
attached = true;
|
|
853
|
-
|
|
854
|
-
await sendCommand(target, "
|
|
855
|
-
|
|
856
|
-
const ctx = makeContext("batch", tabId, item.url, options, target);
|
|
857
|
-
if (config.beforeScroll) {
|
|
858
|
-
await config.beforeScroll(ctx);
|
|
1679
|
+
network.reset();
|
|
1680
|
+
await sendCommand(target, "Network.enable", {}, signal);
|
|
1681
|
+
await preparePageForCapture(target, signal);
|
|
859
1682
|
}
|
|
860
|
-
await
|
|
861
|
-
await
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
await
|
|
866
|
-
return { fileName, warning: output.warning || "" };
|
|
1683
|
+
await prepareCapture(target, item, options, run, network);
|
|
1684
|
+
const output = await capturePrepared(ctx, options, run, network);
|
|
1685
|
+
throwIfAborted(signal);
|
|
1686
|
+
item.warning = [item.warning, output.warning].filter(Boolean).join(" ");
|
|
1687
|
+
item.fileName = buildBatchFileName(job.folder, item, output.extension);
|
|
1688
|
+
await saveOutput(item, output, signal, () => persist(job));
|
|
867
1689
|
} finally {
|
|
1690
|
+
network.dispose();
|
|
868
1691
|
if (attached) {
|
|
1692
|
+
await restoreCaptureDecorations(target).catch(() => {
|
|
1693
|
+
});
|
|
869
1694
|
await debuggerDetach(target).catch(() => {
|
|
870
1695
|
});
|
|
871
1696
|
}
|
|
872
1697
|
await chrome.tabs.remove(tabId).catch(() => {
|
|
873
1698
|
});
|
|
1699
|
+
await saveTabLease(run);
|
|
874
1700
|
}
|
|
875
1701
|
}
|
|
876
|
-
function makeContext(mode, tabId, url, options, target) {
|
|
1702
|
+
function makeContext(mode, tabId, url, options, target, signal) {
|
|
877
1703
|
return {
|
|
878
1704
|
mode,
|
|
879
1705
|
tabId,
|
|
880
1706
|
url,
|
|
881
1707
|
options,
|
|
882
|
-
send: (method, params) => sendCommand(target, method, params)
|
|
1708
|
+
send: (method, params) => sendCommand(target, method, params, signal),
|
|
1709
|
+
cleanup: (method, params) => sendCommand(target, method, params)
|
|
883
1710
|
};
|
|
884
1711
|
}
|
|
885
|
-
function publicBatch() {
|
|
886
|
-
return {
|
|
887
|
-
running: batch.running,
|
|
888
|
-
total: batch.total,
|
|
889
|
-
done: batch.done,
|
|
890
|
-
success: batch.success,
|
|
891
|
-
failed: batch.failed,
|
|
892
|
-
current: batch.current,
|
|
893
|
-
folder: batch.folder,
|
|
894
|
-
freeLimited: batch.freeLimited,
|
|
895
|
-
needVerify: batch.needVerify,
|
|
896
|
-
items: batch.items.map((item) => ({
|
|
897
|
-
url: item.url,
|
|
898
|
-
index: item.index,
|
|
899
|
-
status: item.status,
|
|
900
|
-
fileName: item.fileName,
|
|
901
|
-
error: item.error,
|
|
902
|
-
warning: item.warning
|
|
903
|
-
}))
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
async function persistBatch() {
|
|
907
|
-
await storageSet({ [BATCH_KEY]: batch });
|
|
908
|
-
}
|
|
909
1712
|
async function restoreBatch() {
|
|
910
|
-
const data = await storageGet(BATCH_KEY);
|
|
1713
|
+
const data = await storageGet([BATCH_KEY, QUICK_KEY]);
|
|
1714
|
+
const lease = (await storageCall("session", "get", TAB_KEY))[TAB_KEY];
|
|
1715
|
+
if (Number.isInteger(lease?.tabId) || Number.isInteger(lease?.windowId)) {
|
|
1716
|
+
if (Number.isInteger(lease.tabId))
|
|
1717
|
+
await chrome.tabs.remove(lease.tabId).catch(() => {
|
|
1718
|
+
});
|
|
1719
|
+
await closeIdleTab(lease);
|
|
1720
|
+
await storageCall("session", "remove", TAB_KEY);
|
|
1721
|
+
}
|
|
1722
|
+
await recoverQuick();
|
|
911
1723
|
const saved = data[BATCH_KEY];
|
|
912
|
-
if (!saved?.items
|
|
913
|
-
|
|
1724
|
+
if (!Array.isArray(saved?.items) || !saved.items.length) return;
|
|
1725
|
+
const items = saved.items.filter(
|
|
1726
|
+
(item) => typeof item?.url === "string" && normalizeUrls([item.url]).length
|
|
1727
|
+
).slice(0, 500);
|
|
1728
|
+
batch = {
|
|
1729
|
+
...idleBatch(),
|
|
1730
|
+
...saved,
|
|
1731
|
+
version: 2,
|
|
1732
|
+
settings: normalizeSettings(
|
|
1733
|
+
config.defaultSettings,
|
|
1734
|
+
config.migrateSettings ? config.migrateSettings(saved.settings) : saved.settings
|
|
1735
|
+
),
|
|
1736
|
+
needVerify: null,
|
|
1737
|
+
items: items.map((item, i) => ({
|
|
1738
|
+
...makeItem(item.url, i + 1),
|
|
1739
|
+
...item,
|
|
1740
|
+
chargeFree: item.chargeFree ?? !saved.pro,
|
|
1741
|
+
status: ["success", "failed", "cancelled", "downloading"].includes(
|
|
1742
|
+
item.status
|
|
1743
|
+
) ? item.status : "pending"
|
|
1744
|
+
}))
|
|
1745
|
+
};
|
|
1746
|
+
batch.total = batch.items.length;
|
|
1747
|
+
if (!batch.folder)
|
|
1748
|
+
batch.folder = `${config.brand}/${folderNameFor(batch.items.map((i) => i.url))}`;
|
|
1749
|
+
recount(batch);
|
|
1750
|
+
if (saved.running) {
|
|
1751
|
+
const run = reserveRun(true);
|
|
1752
|
+
run.job = batch;
|
|
1753
|
+
if (saved.cancelRequested) run.controller.abort();
|
|
1754
|
+
batch.current = saved.cancelRequested ? "Stopping\u2026" : "Resuming interrupted batch\u2026";
|
|
1755
|
+
await persist(batch);
|
|
1756
|
+
void runBatch(batch, run);
|
|
914
1757
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1758
|
+
}
|
|
1759
|
+
async function handleMessage(message) {
|
|
1760
|
+
await restorePromise;
|
|
1761
|
+
switch (message?.type) {
|
|
1762
|
+
case `${prefix}_START`:
|
|
1763
|
+
return startBatch(message.urls, message.settings);
|
|
1764
|
+
case `${prefix}_CAPTURE_CURRENT`:
|
|
1765
|
+
case `${prefix}_CONVERT_CURRENT`:
|
|
1766
|
+
return quickCapture(message.tabId, message.settings);
|
|
1767
|
+
case `${prefix}_CANCEL`:
|
|
1768
|
+
if (activeRun) {
|
|
1769
|
+
activeRun.controller.abort();
|
|
1770
|
+
if (activeRun.job) {
|
|
1771
|
+
activeRun.job.cancelRequested = true;
|
|
1772
|
+
activeRun.job.current = "Stopping\u2026";
|
|
1773
|
+
await persist(activeRun.job);
|
|
1774
|
+
}
|
|
1775
|
+
} else if (batch.running) {
|
|
1776
|
+
const run = reserveRun(true);
|
|
1777
|
+
if (run) {
|
|
1778
|
+
run.job = batch;
|
|
1779
|
+
batch.cancelRequested = true;
|
|
1780
|
+
batch.current = "Stopping\u2026";
|
|
1781
|
+
run.controller.abort();
|
|
1782
|
+
await persist(batch).catch(() => {
|
|
1783
|
+
});
|
|
1784
|
+
void runBatch(batch, run);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return { ok: true, batch: publicBatch() };
|
|
1788
|
+
case `${prefix}_CLEAR_BATCH`: {
|
|
1789
|
+
const run = reserveRun(true);
|
|
1790
|
+
if (!run)
|
|
1791
|
+
return {
|
|
1792
|
+
ok: false,
|
|
1793
|
+
error: "Stop the running task before clearing it"
|
|
1794
|
+
};
|
|
1795
|
+
try {
|
|
1796
|
+
run.job = batch;
|
|
1797
|
+
if (batch.running || batch.items.some((item) => item.status === "downloading")) {
|
|
1798
|
+
batch.cancelRequested = true;
|
|
1799
|
+
run.controller.abort();
|
|
1800
|
+
await runBatch(batch, run, { keepLock: true });
|
|
1801
|
+
if (batch.running)
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
"Downloads could not be confirmed. Results were kept; retry Stop or Clear."
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
await storageRemove(BATCH_KEY);
|
|
1807
|
+
batch = idleBatch();
|
|
1808
|
+
} finally {
|
|
1809
|
+
releaseRun(run);
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
ok: true,
|
|
1813
|
+
batch: publicBatch(),
|
|
1814
|
+
quota: await getQuota(true)
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
case `${prefix}_SKIP_VERIFY`:
|
|
1818
|
+
if (activeRun) activeRun.skipVerify = true;
|
|
1819
|
+
return { ok: true };
|
|
1820
|
+
case `${prefix}_STATE`:
|
|
1821
|
+
return {
|
|
1822
|
+
ok: true,
|
|
1823
|
+
batch: publicBatch(),
|
|
1824
|
+
quota: await getQuota(true)
|
|
1825
|
+
};
|
|
1826
|
+
case `${prefix}_ACTIVATE`:
|
|
1827
|
+
await licensing.activate(message.code);
|
|
1828
|
+
return { ok: true, quota: await getQuota(true) };
|
|
1829
|
+
case `${prefix}_REFRESH_LICENSE`:
|
|
1830
|
+
return { ok: true, quota: await getQuota(false, true) };
|
|
1831
|
+
case `${prefix}_CLEAR_LICENSE`:
|
|
1832
|
+
await licensing.deactivate();
|
|
1833
|
+
return { ok: true, quota: await getQuota(true) };
|
|
1834
|
+
default:
|
|
1835
|
+
return { ok: false, error: "Unknown message" };
|
|
945
1836
|
}
|
|
946
1837
|
}
|
|
1838
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1839
|
+
if (message?.target === "offscreen" || sender.id && sender.id !== chrome.runtime.id)
|
|
1840
|
+
return false;
|
|
1841
|
+
handleMessage(message).then(sendResponse).catch(
|
|
1842
|
+
(error) => sendResponse({ ok: false, error: normalizeError(error) })
|
|
1843
|
+
);
|
|
1844
|
+
return true;
|
|
1845
|
+
});
|
|
1846
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1847
|
+
void restorePromise.catch(() => {
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
947
1850
|
function setBadge(text) {
|
|
948
1851
|
chrome.action.setBadgeText({ text });
|
|
949
1852
|
chrome.action.setBadgeBackgroundColor({ color: config.badgeColor });
|
|
950
1853
|
}
|
|
1854
|
+
restorePromise = restoreBatch();
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/capture-budget.js
|
|
1858
|
+
var CAPTURE_LIMITS = {
|
|
1859
|
+
maxEdgeDevicePx: 16384,
|
|
1860
|
+
maxDevicePixels: 64 * 1024 * 1024
|
|
1861
|
+
};
|
|
1862
|
+
function captureScale(dpr) {
|
|
1863
|
+
return Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
|
|
1864
|
+
}
|
|
1865
|
+
function captureFits(width, height, dpr) {
|
|
1866
|
+
const scale = captureScale(dpr);
|
|
1867
|
+
return [width, height].every((n) => Number.isFinite(n) && n > 0) && width * scale <= CAPTURE_LIMITS.maxEdgeDevicePx && height * scale <= CAPTURE_LIMITS.maxEdgeDevicePx && width * height * scale ** 2 <= CAPTURE_LIMITS.maxDevicePixels;
|
|
1868
|
+
}
|
|
1869
|
+
function assertCaptureFits(width, height, dpr, message) {
|
|
1870
|
+
if (!captureFits(width, height, dpr)) throw new Error(message);
|
|
1871
|
+
}
|
|
1872
|
+
function captureBudget(dpr) {
|
|
1873
|
+
const scale = captureScale(dpr);
|
|
1874
|
+
return {
|
|
1875
|
+
maxEdge: Math.floor(CAPTURE_LIMITS.maxEdgeDevicePx / scale),
|
|
1876
|
+
maxArea: Math.floor(CAPTURE_LIMITS.maxDevicePixels / scale ** 2)
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
async function readDevicePixelRatio(ctx) {
|
|
1880
|
+
const result = await ctx.send("Runtime.evaluate", {
|
|
1881
|
+
expression: "devicePixelRatio || 1",
|
|
1882
|
+
returnByValue: true
|
|
1883
|
+
});
|
|
1884
|
+
return captureScale(result?.result?.value);
|
|
951
1885
|
}
|
|
952
1886
|
|
|
953
1887
|
// src/background.entry.ts
|
|
954
|
-
globalThis.ZuckerExt = {
|
|
1888
|
+
globalThis.ZuckerExt = {
|
|
1889
|
+
init: initExtension,
|
|
1890
|
+
limits: CAPTURE_LIMITS,
|
|
1891
|
+
pageDecorations: { hide: hidePageCaptureDecorations, show: showPageCaptureDecorations },
|
|
1892
|
+
captureFits,
|
|
1893
|
+
assertCaptureFits,
|
|
1894
|
+
captureBudget,
|
|
1895
|
+
readDevicePixelRatio
|
|
1896
|
+
};
|
|
955
1897
|
})();
|